Vlasiator ebf0dd394 on dev (v5.4.0 + 1054 commits)
Loading...
Searching...
No Matches
ionosphere.cpp
Go to the documentation of this file.
1/*
2 * This file is part of Vlasiator.
3 * Copyright 2010-2016 Finnish Meteorological Institute
4 *
5 * For details of usage, see the COPYING file and read the "Rules of the Road"
6 * at http://www.physics.helsinki.fi/vlasiator/
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22
27
28#include <cstdint>
29#include <cstdlib>
30#include <iostream>
31#include <filesystem>
32#include <iomanip>
33#include <fstream>
34#include <sstream>
35
36#include "ionosphere.h"
37#include "../projects/project.h"
43#include "../common.h"
44#include "../object_wrapper.h"
45
46#include <Eigen/Dense>
47#include <unsupported/Eigen/SparseExtra>
49#include "ionosphere_tables.h"
50
51#define Vec3d Eigen::Vector3d
52#define cross_product(av, bv) (av).cross(bv)
53#define dot_product(av, bv) (av).dot(bv)
54#define vector_length(v) (v).norm()
55#define normalize_vector(v) (v).normalized()
56
57#ifdef DEBUG_VLASIATOR
58#ifndef DEBUG_IONOSPHERE
59#define DEBUG_IONOSPHERE
60#endif
61#endif
62#ifdef DEBUG_SYSBOUNDARY
63#ifndef DEBUG_IONOSPHERE
64#define DEBUG_IONOSPHERE
65#endif
66#endif
67
68namespace SBC {
70
72
73 std::vector<IonosphereSpeciesParameters> Ionosphere::speciesParams;
74
75 // Static ionosphere member variables
83
103
104 // Offset field aligned currents so their sum is 0
106
107 if (nodes.size() == 0) {
108 return;
109 }
110
111 // Separately make sure that both hemispheres are divergence-free
112 Real northSum = 0.;
113 int northNum = 0;
114 Real southSum = 0.;
115 int southNum = 0;
116
117 for (uint n = 0; n < nodes.size(); n++) {
118 if (nodes[n].x[2] > 0) {
119 northSum += nodes[n].parameters[ionosphereParameters::SOURCE];
120 northNum++;
121 } else {
122 southSum += nodes[n].parameters[ionosphereParameters::SOURCE];
123 southNum++;
124 }
125 }
126
127 northSum /= northNum;
128 southSum /= southNum;
129
130 for (uint n = 0; n < nodes.size(); n++) {
131 if (nodes[n].x[2] > 0) {
132 nodes[n].parameters[ionosphereParameters::SOURCE] -= northSum;
133 } else {
134 nodes[n].parameters[ionosphereParameters::SOURCE] -= southSum;
135 }
136 }
137 }
138
139 // Scale all nodes' coordinates so that they are situated on a spherical
140 // shell with radius R
142 Real L = sqrt(n.x[0] * n.x[0] + n.x[1] * n.x[1] + n.x[2] * n.x[2]);
143 for (int c = 0; c < 3; c++) {
144 n.x[c] *= R / L;
145 }
146 }
147
148 // Regenerate linking information between nodes and elements
149 // Note: if this runs *before* stitchRefinementInterfaces(), there will be no
150 // more information about t-junctions, so further stitiching won't work
152
153 for (uint n = 0; n < nodes.size(); n++) {
154 nodes[n].numTouchingElements = 0;
155
156 for (uint e = 0; e < elements.size(); e++) {
157 for (int c = 0; c < 3; c++) {
158 if (elements[e].corners[c] == n) {
159 nodes[n].touchingElements[nodes[n].numTouchingElements++] = e;
160 }
161 }
162 }
163 }
164 }
165
166 // Initialize base grid as a tetrahedron
168 // clang-format off
169 const static std::array<uint32_t, 3> seedElements[4] = {
170 {1,2,3}, {1,3,4}, {1,4,2}, {2,4,3}
171 };
172 const static std::array<Real, 3> nodeCoords[4] = {
173 { 0., 0., 1.73205},
174 { 0., 1.63299, -0.57735},
175 {-1.41421,-0.816497,-0.57735},
176 { 1.41421,-0.816497,-0.57735}
177 };
178 // clang-format on
179
180 // Create nodes
181 // Additional nodes from table
182 for (const auto& coords : nodeCoords) {
183 Node newNode;
184 newNode.x = coords;
186 nodes.push_back(newNode);
187 }
188
189 // Create elements
190 for (const auto& seed : seedElements) {
191 Element newElement;
192 newElement.corners = seed;
193 elements.push_back(newElement);
194 }
195
196 // Link elements to nodes
198 }
199
200 // Initialize base grid as an Octahedron
202 // clang-format off
203 const static std::array<uint32_t, 3> seedElements[8] = {
204 {0,1,2}, {0,2,3}, {0,3,4}, {0,4,1},
205 {5,2,1}, {5,3,2}, {5,4,3}, {5,1,4},
206 };
207 const static std::array<Real, 3> nodeCoords[6] = {
208 { 0, 0, 1},
209 { 1, 0, 0},
210 { 0, 1, 0},
211 {-1, 0, 0},
212 { 0,-1, 0},
213 { 0, 0,-1}
214 };
215 // clang-format on
216
217 // Create nodes
218 // Additional nodes from table
219 for (const auto& coords : nodeCoords) {
220 Node newNode;
221 newNode.x = coords;
223 nodes.push_back(newNode);
224 }
225
226 // Create elements
227 for (const auto& seed : seedElements) {
228 Element newElement;
229 newElement.corners = seed;
230 elements.push_back(newElement);
231 }
232
233 // Link elements to nodes
235 }
236
237 // Initialize base grid as a icosahedron
239 // clang-format off
240 const static std::array<uint32_t, 3> seedElements[20] = {
241 { 0, 2, 1}, { 0, 3, 2}, { 0, 4, 3}, { 0, 5, 4},
242 { 0, 1, 5}, { 1, 2, 6}, { 2, 3, 7}, { 3, 4, 8},
243 { 4, 5, 9}, { 5, 1,10}, { 6, 2, 7}, { 7, 3, 8},
244 { 8, 4, 9}, { 9, 5,10}, {10, 1, 6}, { 6, 7,11},
245 { 7, 8,11}, { 8, 9,11}, { 9,10,11}, {10, 6,11}
246 };
247 const static std::array<Real, 3> nodeCoords[12] = {
248 { 0., 0., 1.17557}, { 1.05146, 0., 0.525731},
249 { 0.32492, 1.0, 0.525731}, {-0.850651, 0.618034, 0.525731},
250 {-0.850651,-0.618034, 0.525731}, { 0.32492, -1.0, 0.525731},
251 { 0.850651, 0.618034,-0.525731}, {-0.32492, 1.0, -0.525731},
252 {-1.051460, 0. ,-0.525731}, {-0.32492, -1.0, -0.525731},
253 { 0.850651,-0.618034,-0.525731}, { 0. , 0., -1.17557}
254 };
255 // clang-format on
256
257 // Create nodes
258 // Additional nodes from table
259 for (const auto& coords : nodeCoords) {
260 Node newNode;
261 newNode.x = coords;
263 nodes.push_back(newNode);
264 }
265
266 // Create elements
267 for (const auto& seed : seedElements) {
268 Element newElement;
269 newElement.corners = seed;
270 elements.push_back(newElement);
271 }
272
273 // Link elements to nodes
275 }
276
277 // Spherical fibonacci base grid with arbitrary number of nodes n>8,
278 // after Keinert et al 2015
280
281 phiprof::Timer timer{"ionosphere-sphericalFibonacci"};
282 // Golden ratio
283 const Real Phi = (sqrt(5) + 1.) / 2.;
284
285 auto madfrac = [](Real a, Real b) -> float { return a * b - floor(a * b); };
286
287 // Forward spherical fibonacci mapping with n points
288 auto SF = [madfrac, Phi](int i, int n) -> Vec3d {
289 Real phi = 2 * M_PI * madfrac(i, Phi - 1.);
290 Real z = 1. - (2. * i + 1.) / n;
291 Real sinTheta = sqrt(1 - z * z);
292 return {cos(phi) * sinTheta, sin(phi) * sinTheta, z};
293 };
294
295 // Sample delaunay triangulation of the spherical fibonaccy grid around the given
296 // point and return adjacent vertices
297 auto SFDelaunayAdjacency = [SF, Phi](int j, int n) -> std::vector<int> {
298 Real cosTheta = 1. - (2. * j + 1.) / n;
299 Real z = max(0., round(0.5 * log(n * M_PI * sqrt(5) * (1. - cosTheta * cosTheta)) / log(Phi)));
300
301 Vec3d nearestSample = SF(j, n);
302 std::vector<int> nearestSamples;
303
304 // Sample neighbourhood to find closest neighbours
305 // Magic rainbow indexing
306 for (int i = 0; i < 12; i++) {
307 int r = i - floor(i / 6) * 6;
308 int c = 5 - abs(5 - r * 2) + floor((int)r / 3);
309 int k = j + (i < 6 ? +1 : -1) * (int)round(pow(Phi, z + c - 2) / sqrt(5.));
310
311 Vec3d currentSample = SF(k, n);
312 Vec3d nearestToCurrentSample = currentSample - nearestSample;
313 Real squaredDistance = dot_product(nearestToCurrentSample, nearestToCurrentSample);
314
315 // Early reject by invalid index and distance
316 if (k < 0 || k >= n || squaredDistance > 5. * 4. * M_PI / (sqrt(5) * n)) {
317 continue;
318 }
319
320 nearestSamples.push_back(k);
321 }
322
323 // Make it delaunay
324 std::vector<int> adjacentVertices;
325 for (int i = 0; i < (int)nearestSamples.size(); i++) {
326 int k = nearestSamples[i];
327 int kPrevious = nearestSamples[(i + nearestSamples.size() - 1) % nearestSamples.size()];
328 int kNext = nearestSamples[(i + 1) % nearestSamples.size()];
329
330 Vec3d currentSample = SF(k, n);
331 Vec3d previousSample = SF(kPrevious, n);
332 Vec3d nextSample = SF(kNext, n);
333
334 if (dot_product(previousSample - nextSample, previousSample - nextSample) > dot_product(currentSample - nearestSample, currentSample - nearestSample)) {
335 adjacentVertices.push_back(nearestSamples[i]);
336 }
337 }
338
339 // Special case for the pole
340 if (j == 0) {
341 adjacentVertices.pop_back();
342 }
343
344 return adjacentVertices;
345 };
346
347 // Create nodes
348 for (int i = 0; i < n; i++) {
349 Node newNode;
350
351 Vec3d pos = SF(i, n);
352 newNode.x = {pos[0], pos[1], pos[2]};
354
355 nodes.push_back(newNode);
356 }
357
358 // Create elements
359 for (int i = 0; i < n; i++) {
360 std::vector<int> neighbours = SFDelaunayAdjacency(i, n);
361
362 // Build a triangle fan around the neighbourhood
363 for (uint j = 0; j < neighbours.size(); j++) {
364 if (neighbours[j] > i && neighbours[(j + 1) % neighbours.size()] > i) {
365 // Only triangles in "positive" direction to avoid double cover.
366 Element newElement;
367 newElement.corners = {(uint)i, (uint)neighbours[j], (uint)neighbours[(j + 1) % neighbours.size()]};
368 elements.push_back(newElement);
369 }
370 }
371 }
372
374 }
375
376 // Read an arbitrary ionosphere grid, in either Wavefront OBJ
377 // or VTK ASCII format.
378 // The file should contain only a single triangle grid (and be otherwise
379 // reasonable for use as an ionosphere mesh)
381 filesystem::path path = pathString;
382 ifstream fi;
383 fi.open(pathString.c_str());
384 if (!fi.is_open()) {
385 cerr << "(IONOSPHERE) Could not open file: " << pathString << endl;
386 abort();
387 }
388 string line;
389 if (path.extension() == ".obj") {
390 while (getline(fi, line)) {
391 // Ignore all data other than vertices and faces
392 if (!(line.rfind("v\t", 0) == 0 || line.rfind("v ", 0) == 0 || line.rfind("f", 0) == 0)) {
393 continue;
394 }
395
396 // Read vertices
397 while (line.rfind("v ", 0) == 0) {
398 istringstream ss(line.substr(1));
399 double num1, num2, num3;
400 if (!(ss >> num1 >> num2 >> num3)) {
401 cerr << "(IONOSPHERE) Error reading vertex information of line \"" << line << "\" in " << pathString << endl;
402 abort();
403 }
404 Node newNode;
405 newNode.x = {num1, num2, num3};
407 nodes.push_back(newNode);
408 getline(fi, line);
409 }
410
411 int length = nodes.size();
412 // Read faces, support negative number specification
413 while (line.rfind("f", 0) == 0) {
414 istringstream ss(line.substr(1));
415 string faceArg;
416 std::vector<int> vertexIndices;
417 // Ignore normal and texture vertices
418 while (ss >> faceArg) {
419 istringstream fss(faceArg);
420 int v;
421 if (!(fss >> v)) {
422 cerr << "(IONOSPHERE) Error reading face information of line \"" << line << "\" in " << pathString << endl;
423 abort();
424 }
425 // Support negative indices (indices are 1-indexed)
426 if (v < 0) {
427 v = length + v;
428 } else {
429 v = v - 1;
430 }
431 if (v < 0 || v >= length) {
432 cerr << "(IONOSPHERE) Invalid vertex index (" << v << ") specified in \"" << line << "\" in " << pathString << endl;
433 abort();
434 }
435 vertexIndices.push_back(v);
436 }
437 if (vertexIndices.size() != 3) {
438 cerr << "(IONOSPHERE) Too many vertex indices (" << vertexIndices.size() << ") specified in \"" << line << "\" in " << pathString << " (Only triangulated meshes are supported)" << endl;
439 abort();
440 }
441 Element newElement;
442 newElement.corners = std::array<uint32_t, 3>{(uint32_t)vertexIndices[0], (uint32_t)vertexIndices[1], (uint32_t)vertexIndices[2]};
443 elements.push_back(newElement);
444 getline(fi, line);
445 }
446 }
447
448 if (nodes.size() == 0) {
449 cerr << "(IONOSPHERE) Error reading nodes in \"" << pathString << "\", expected a non-zero number of nodes to be specified." << endl;
450 abort();
451 }
452
453 if (elements.size() == 0) {
454 cerr << "(IONOSPHERE) Error reading faces in \"" << pathString << "\", expected a non-zero number of faces to be specified." << endl;
455 abort();
456 }
457 } else if (path.extension() == ".vtk") {
458 if (!getline(fi, line)) {
459 cerr << "(IONOSPHERE) Error reading version string in " << pathString << endl;
460 abort();
461 }
462 if (!(line.rfind("# vtk DataFile Version ", 0) == 0)) {
463 cerr << "(IONOSPHERE) Expected mandatory VTK version string, obtained \"" << line << "\" in " << pathString << endl;
464 abort();
465 }
466 float version = stof(line.substr(23));
467 if (version > 4.2) {
468 cerr << "(IONOSPHERE) VTK version unsupported, expected legacy version less than 4.2, instead obtained " << version << " in " << pathString << endl;
469 abort();
470 }
471 if (!getline(fi, line)) {
472 cerr << "(IONOSPHERE) Error reading mandatory description string in " << pathString << endl;
473 abort();
474 }
475 if (!getline(fi, line)) {
476 cerr << "(IONOSPHERE) Error reading mandatory data type string in " << pathString << ", ASCII or BINARY data not specified." << endl;
477 abort();
478 }
479 if (line != "ASCII") {
480 cerr << "(IONOSPHERE) Only ASCII VTK data is supported, obtained " << line << endl;
481 abort();
482 }
483
484 if (getline(fi, line)) {
485 stringstream ss(line);
486 string dataset;
487 string data;
488 if (ss >> dataset >> data) {
489 if (dataset != "DATASET" || data != "UNSTRUCTURED_GRID") {
490 cerr << "(IONOSPHERE) Could not find DATASET specification in " << pathString << endl;
491 abort();
492 }
493 }
494 } else {
495 cerr << "(IONOSPHERE) Error reading mandatory DATASET string in " << pathString << endl;
496 abort();
497 }
498
499 if (getline(fi, line)) {
500 std::vector<Real> coords;
501 stringstream pss(line);
502 string points;
503 unsigned int size;
504 string type;
505
506 if (!(pss >> points >> size >> type)) {
507 cerr << "(IONOSPHERE) Could not read POINTS field \"" << line << "\"" << " in " << pathString << endl;
508 abort();
509 }
510
511 if (!(points == "POINTS")) {
512 cerr << "(IONOSPHERE) Mandatory POINTS field not found, obtained " << line << "\" in " << pathString << endl;
513 abort();
514 }
515
516 if (type != "float" && type != "double") {
517 cerr << "(IONOSPHERE) Only float or double are supported, obtained \"" << type << "\" in " << pathString << endl;
518 abort();
519 }
520
521 while (getline(fi, line) && all_of(line.begin(), line.end(), [](char c) { return c == 'e' || c == 'E' || c == '+' || c == '.' || c == ' ' || c == '-' || isdigit(c); })) {
522 stringstream css(line);
523 double x;
524 while (css >> x) {
525 coords.push_back(x);
526 }
527 }
528
529 if (coords.size() != size * 3) {
530 cerr << "(IONOSPHERE) Number of coordinates in POINTS field (" << size * 3 << ") does not match number of coordinates found (" << coords.size() << ") in " << pathString << endl;
531 abort();
532 }
533
534 for (unsigned int i = 0; i < coords.size(); i += 3) {
535 Node newNode;
536 newNode.x = {coords[i], coords[i + 1], coords[i + 2]};
538 nodes.push_back(newNode);
539 }
540
541 } else {
542 cerr << "(IONOSPHERE) Could not read POINTS field in " << pathString << endl;
543 abort();
544 }
545
546 if (!fi.eof()) {
547 stringstream css(line);
548 string cells;
549 unsigned int cellNum;
550 unsigned int size;
551
552 if (!(css >> cells >> cellNum >> size)) {
553 cerr << "(IONOSPHERE) Could not read CELLS field \"" << line << "\"" << " in " << pathString << endl;
554 abort();
555 }
556
557 if (!(cells == "CELLS")) {
558 cerr << "(IONOSPHERE) Mandatory CELLS field not found, obtained " << line << "\" in " << pathString << endl;
559 abort();
560 }
561
562 if (!(cellNum * 4 == size)) {
563 cerr << "(IONOSPHERE) Incorrect number of entries for the corresponding number of cells, obtained " << line << "\" in " << pathString << endl;
564 abort();
565 }
566
567 while (getline(fi, line) && all_of(line.begin(), line.end(), [](char c) { return c == ' ' || isdigit(c); })) {
568 stringstream css(line);
569 unsigned int t, a, b, c;
570 while (css >> t >> a >> b >> c) {
571 if (!(t == 3)) {
572 cerr << "(IONOSPHERE) Non-triangular cell encountered, \"" << line << "\" in " << pathString << endl;
573 abort();
574 }
575 for (int v : {a, b, c}) {
576 if (v < 0 || (unsigned int)v >= nodes.size()) {
577 cerr << "(IONOSPHERE) Error vertex number out of bounds, " << v << " in \"" << line << "\" in " << pathString << endl;
578 abort();
579 }
580 }
581 Element newElement;
582 newElement.corners = {a, b, c};
583 elements.push_back(newElement);
584 }
585 }
586
587 if (elements.size() != cellNum) {
588 cerr << "(IONOSPHERE) Number of cells does not match file, expected " << cellNum << ", obtained " << elements.size() << " in " << pathString << endl;
589 abort();
590 }
591 } else {
592 cerr << "(IONOSPHERE) Could not read CELLS field \"" << line << "\"" << " in " << pathString << endl;
593 abort();
594 }
595
596 } else {
597 cerr << "(IONOSPHERE) Unknown ionosphere grid mesh file format " << path.extension() << endl;
598 abort();
599 }
600
602 }
603
604 // Find the neighbouring element of the one with index e, that is sharing the
605 // two corner nodes n1 and n2
606 //
607 // 2 . . . . . . . .*
608 // / \ .
609 // / \ neigh .
610 // / \ bour .
611 // / e \ .
612 // / \ .
613 // / \ .
614 // / \ .
615 // 0----------------1
616 //
617 int32_t SphericalTriGrid::findElementNeighbour(uint32_t e, int n1, int n2) {
618 Element& el = elements[e];
619
620 Node& node1 = nodes[el.corners[n1]];
621 Node& node2 = nodes[el.corners[n2]];
622
623 for (uint n1e = 0; n1e < node1.numTouchingElements; n1e++) {
624 if (node1.touchingElements[n1e] == e)
625 continue; // Skip ourselves.
626
627 for (uint n2e = 0; n2e < node2.numTouchingElements; n2e++) {
628 if (node1.touchingElements[n1e] == node2.touchingElements[n2e]) {
629 return node1.touchingElements[n1e];
630 }
631 }
632 }
633
634 // No neighbour found => Apparently, the neighbour is refined and doesn't
635 // exist at this scale. Good enough for us.
636 return -1;
637 }
638
639 // Find the mesh node closest to the given coordinates.
640 uint32_t SphericalTriGrid::findNodeAtCoordinates(std::array<Real, 3> x) {
641
642 // Project onto sphere
643 Real L = sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]);
644 for (int c = 0; c < 3; c++) {
645 x[c] *= Ionosphere::innerRadius / L;
646 }
647
648 uint32_t node = 0;
649 uint32_t nextNode = 0;
650
651 // TODO: For spherical fibonacci meshes, this can be accelerated by
652 // doing an iSF lookup
653
654 // Iterate through nodes to find the closest one
655 while (true) {
656
657 node = nextNode;
658
659 // This nodes' distance to our target point
660 std::array<Real, 3> deltaX({x[0] - nodes[node].x[0], x[1] - nodes[node].x[1], x[2] - nodes[node].x[2]});
661 Real minDist = sqrt(deltaX[0] * deltaX[0] + deltaX[1] * deltaX[1] + deltaX[2] * deltaX[2]);
662
663 // Iterate through our neighbours
664 for (uint i = 0; i < nodes[node].numTouchingElements; i++) {
665 for (int j = 0; j < 3; j++) {
666 uint32_t thatNode = elements[nodes[node].touchingElements[i]].corners[j];
667 if (thatNode == node || thatNode == nextNode) {
668 continue;
669 }
670
671 // If it is closer, continue there.
672 deltaX = {x[0] - nodes[thatNode].x[0], x[1] - nodes[thatNode].x[1], x[2] - nodes[thatNode].x[2]};
673 Real thatDist = sqrt(deltaX[0] * deltaX[0] + deltaX[1] * deltaX[1] + deltaX[2] * deltaX[2]);
674 if (thatDist < minDist) {
675 minDist = thatDist;
676 nextNode = thatNode;
677 }
678 }
679 }
680
681 // Didn't find a closer one, use this one.
682 if (nextNode == node) {
683 break;
684 }
685 }
686
687 return node;
688 }
689
690 // Subdivide mesh within element e
691 // The element gets replaced by four new ones:
692 //
693 /* 2 2
694 // / \ / \
695 // / \ / 2 \
696 // / \ / \
697 // / \ ==> 2--------1
698 // / \ / \ 3 / \
699 // / \ / 0 \ / 1 \
700 // / \ / \ / \
701 // 0----------------1 0-------0--------1
702 */
703 // And three new nodes get created at the interfaces,
704 // unless they already exist.
705 // The new center element (3) replaces the old parent element in place.
707
708 phiprof::Timer timer{"ionosphere-subdivideElement"};
709 Element& parentElement = elements[e];
710
711 // 4 new elements
712 std::array<Element, 4> newElements;
713 for (int i = 0; i < 4; i++) {
714 newElements[i].refLevel = parentElement.refLevel + 1;
715 }
716
717 // (up to) 3 new nodes
718 std::array<uint32_t, 3> edgeNodes;
719 for (int i = 0; i < 3; i++) { // Iterate over the edges of the triangle
720
721 // Taking the two nodes on that edge
722 Node& n1 = nodes[parentElement.corners[i]];
723 Node& n2 = nodes[parentElement.corners[(i + 1) % 3]];
724
725 // Find the neighbour in that direction
726 int32_t ne = findElementNeighbour(e, i, (i + 1) % 3);
727
728 if (ne == -1) { // Neighbour is refined already, node should already exist.
729
730 // Find it.
731 int32_t insertedNode = -1;
732
733 // First assemble a list of candidates from all elements touching
734 // that corner at the next refinement level
735 std::set<uint32_t> candidates;
736 for (uint en = 0; en < n1.numTouchingElements; en++) {
737 if (elements[n1.touchingElements[en]].refLevel == parentElement.refLevel + 1) {
738 for (int k = 0; k < 3; k++) {
739 candidates.emplace(elements[n1.touchingElements[en]].corners[k]);
740 }
741 }
742 }
743 // Then match that list from the second corner
744 for (uint en = 0; en < n2.numTouchingElements; en++) {
745 if (elements[n2.touchingElements[en]].refLevel == parentElement.refLevel + 1) {
746 for (int k = 0; k < 3; k++) {
747 if (candidates.count(elements[n2.touchingElements[en]].corners[k]) > 0) {
748 insertedNode = elements[n2.touchingElements[en]].corners[k];
749 }
750 }
751 }
752 }
753 if (insertedNode == -1) {
754 cerr << "(IONOSPHERE) Warning: did not find neighbouring split node when trying to refine "
755 << "element " << e << " on edge " << i << " with nodes ("
756 << parentElement.corners[0] << ", " << parentElement.corners[1] << ", " << parentElement.corners[2]
757 << ")" << endl;
758 insertedNode = 0;
759 }
760
761 // Double-check that this node currently has 4 touching elements
762 if (nodes[insertedNode].numTouchingElements != 4) {
763 cerr << "(IONOSPHERE) Warning: mesh topology screwup when refining: node " << insertedNode
764 << " is touching " << nodes[insertedNode].numTouchingElements << " elements, should be 4." << endl;
765 }
766
767 // Add the other 2
768 nodes[insertedNode].touchingElements[4] = elements.size() + i;
769 nodes[insertedNode].touchingElements[5] = elements.size() + (i + 1) % 3;
770
771 // Now that node touches 6 elements.
772 nodes[insertedNode].numTouchingElements = 6;
773
774 edgeNodes[i] = insertedNode;
775
776 } else { // Neighbour is not refined, add a node here.
777 Node newNode;
778
779 // Node coordinates are in the middle of the two parents
780 for (int c = 0; c < 3; c++) {
781 newNode.x[c] = 0.5 * (n1.x[c] + n2.x[c]);
782 }
783 // Renormalize to sit on the circle
785
786 // This node has four touching elements: the old neighbour and 3 of the new ones
787 newNode.numTouchingElements = 4;
788 newNode.touchingElements[0] = ne;
789 newNode.touchingElements[1] = e; // Center element
790 newNode.touchingElements[2] = elements.size() + i;
791 newNode.touchingElements[3] = elements.size() + (i + 1) % 3;
792
793 nodes.push_back(newNode);
794 edgeNodes[i] = nodes.size() - 1;
795 }
796 }
797
798 // Now set the corners of the new elements
799 newElements[0].corners[0] = parentElement.corners[0];
800 newElements[0].corners[1] = edgeNodes[0];
801 newElements[0].corners[2] = edgeNodes[2];
802 newElements[1].corners[0] = edgeNodes[0];
803 newElements[1].corners[1] = parentElement.corners[1];
804 newElements[1].corners[2] = edgeNodes[1];
805 newElements[2].corners[0] = edgeNodes[2];
806 newElements[2].corners[1] = edgeNodes[1];
807 newElements[2].corners[2] = parentElement.corners[2];
808 newElements[3].corners[0] = edgeNodes[0];
809 newElements[3].corners[1] = edgeNodes[1];
810 newElements[3].corners[2] = edgeNodes[2];
811
812 // And references of the corners are replaced to point
813 // to the new child elements
814 for (int n = 0; n < 3; n++) {
815 Node& cornerNode = nodes[parentElement.corners[n]];
816 for (uint i = 0; i < cornerNode.numTouchingElements; i++) {
817 if (cornerNode.touchingElements[i] == e) {
818 cornerNode.touchingElements[i] = elements.size() + n;
819 }
820 }
821 }
822
823 // The center element replaces the original one
824 elements[e] = newElements[3];
825 // Insert the other new elements at the end of the list
826 for (int i = 0; i < 3; i++) {
827 elements.push_back(newElements[i]);
828 }
829 }
830
831 // Fractional energy dissipation rate for a isotropic beam, based on Rees (1963), figure 1
833 static const Real P[7] = {-11.639, 32.1133, -30.8543, 14.6063, -6.3375, 0.6138, 1.4946};
834 Real lambda = (((((P[0] * x + P[1]) * x + P[2]) * x + P[3]) * x + P[4]) * x + P[5]) * x + P[6];
835 if (x > 1. || lambda < 0) {
836 return 0;
837 }
838 return lambda;
839 }
840
841 // Energy dissipasion function based on Sergienko & Ivanov (1993), eq. A2
843
845 Real E; // in eV
846 Real C1;
847 Real C2;
848 Real C3;
849 Real C4;
850 };
851
852 // clang-format off
853 const static SergienkoIvanovParameters SIparameters[] = {
854 {50, 0.0409, 1.072, -0.0641, -1.054},
855 {100, 0.0711, 0.899, -0.171, -0.720},
856 {500, 0.130, 0.674, -0.271, -0.319},
857 {1000,0.142, 0.657, -0.277, -0.268}
858 };
859 // clang-format on
860
861 Real C1 = 0;
862 Real C2 = 0;
863 Real C3 = 0;
864 Real C4 = 0;
865 if (E0 <= SIparameters[0].E) {
866 C1 = SIparameters[0].C1;
867 C2 = SIparameters[0].C2;
868 C3 = SIparameters[0].C3;
869 C4 = SIparameters[0].C4;
870 } else if (E0 >= SIparameters[3].E) {
871 C1 = SIparameters[3].C1;
872 C2 = SIparameters[3].C2;
873 C3 = SIparameters[3].C3;
874 C4 = SIparameters[3].C4;
875 } else {
876 for (int i = 0; i < 3; i++) {
877 if (SIparameters[i].E < E0 && SIparameters[i + 1].E > E0) {
878 Real interp = (E0 - SIparameters[i].E) / (SIparameters[i + 1].E - SIparameters[i].E);
879 C1 = (1. - interp) * SIparameters[i].C1 + interp * SIparameters[i + 1].C1;
880 C2 = (1. - interp) * SIparameters[i].C2 + interp * SIparameters[i + 1].C2;
881 C3 = (1. - interp) * SIparameters[i].C3 + interp * SIparameters[i + 1].C3;
882 C4 = (1. - interp) * SIparameters[i].C4 + interp * SIparameters[i + 1].C4;
883 }
884 }
885 }
886 return (C2 + C1 * Chi) * exp(C4 * Chi + C3 * Chi * Chi);
887 }
888
889 /* Read atmospheric model file in MSIS format.
890 * Based on the table data, precalculate and fill the ionisation production lookup table
891 */
893
894 phiprof::Timer timer{"ionosphere-readAtmosphericModelFile"};
895 // These are the only height values (in km) we are actually interested in
896 // clang-format off
897 static const float alt[numAtmosphereLevels] = {
898 66, 68, 71, 74, 78, 82, 87, 92, 98, 104, 111,
899 118, 126, 134, 143, 152, 162, 172, 183, 194
900 };
901 // clang-format on
902
903 // Open file, read in
904 ifstream in(filename);
905 if (!in) {
906 cerr << "(ionosphere) WARNING: Atmospheric Model file " << filename << " could not be opened: " << strerror(errno) << endl;
907 cerr << "(ionosphere) All atmospheric values will be zero, and there will be no ionization!" << endl;
908 }
909 int altindex = 0;
910 Real integratedDensity = 0;
911 Real prevDensity = 0;
912 Real prevAltitude = 0;
913 std::vector<std::array<Real, 5>> MSISvalues;
914 while (in) {
915 Real altitude, massdensity, Odensity, N2density, O2density, neutralTemperature;
916 in >> altitude >> Odensity >> N2density >> O2density >> massdensity >> neutralTemperature;
917
918 integratedDensity += (altitude - prevAltitude) * 1000 * 0.5 * (massdensity + prevDensity);
919 // Ion-neutral scattering frequencies (from Schunk and Nagy, 2009, Table 4.5)
920 Real nui = 1e-17 * (3.67 * Odensity + 5.14 * N2density + 2.59 * O2density);
921 // Elctron-neutral scattering frequencies (Same source, Table 4.6)
922 Real nue = 1e-17 * (8.9 * Odensity + 2.33 * N2density + 18.2 * O2density);
923 prevAltitude = altitude;
924 prevDensity = massdensity;
925 MSISvalues.push_back({altitude, massdensity, nui, nue, integratedDensity});
926 }
927
928 // Iterate through the read data and linearly interpolate
929 for (unsigned int i = 1; i < MSISvalues.size(); i++) {
930 Real altitude = MSISvalues[i][0];
931
932 // When we encounter one of our reference layers, record its values
933 while (altindex < numAtmosphereLevels && altitude >= alt[altindex]) {
934 Real interpolationFactor = (alt[altindex] - MSISvalues[i - 1][0]) / (MSISvalues[i][0] - MSISvalues[i - 1][0]);
935
936 AtmosphericLayer newLayer;
937 newLayer.altitude = alt[altindex]; // in km
938 newLayer.density = fmax((1.-interpolationFactor) * MSISvalues[i-1][1] + interpolationFactor * MSISvalues[i][1], 0.); // kg/m^3
939 newLayer.depth = fmax((1.-interpolationFactor) * MSISvalues[i-1][4] + interpolationFactor * MSISvalues[i][4], 0.); // kg/m^2
940 newLayer.nui = fmax((1.-interpolationFactor) * MSISvalues[i-1][2] + interpolationFactor * MSISvalues[i][2], 0.); // m^-3 s^-1
941 newLayer.nue = fmax((1.-interpolationFactor) * MSISvalues[i-1][3] + interpolationFactor * MSISvalues[i][3], 0.); // m^-3 s^-1
942 atmosphere[altindex++] = newLayer;
943 }
944 }
945
946 // Now we have integrated density from the bottom of the atmosphere in the depth field.
947 // Flip it around.
948 for (int h = 0; h < numAtmosphereLevels; h++) {
949 atmosphere[h].depth = integratedDensity - atmosphere[h].depth;
950 }
951
952 // Calculate Hall and Pedersen conductivity coefficient based on charge carrier density
953 const Real Bval = 5e-5;// TODO: Hardcoded B strength here?
954 const Real NO_gyroFreq = physicalconstants::CHARGE * Bval / (31*physicalconstants::MASS_PROTON); // Ion (NO+) gyration frequency
955 const Real e_gyroFreq = physicalconstants::CHARGE * Bval / (physicalconstants::MASS_ELECTRON); // Elctron gyration frequency
956 for(int h=0; h<numAtmosphereLevels; h++) {
959 atmosphere[h].pedersencoeff = sigma_i * (atmosphere[h].nui * atmosphere[h].nui) /( atmosphere[h].nui*atmosphere[h].nui + NO_gyroFreq*NO_gyroFreq)
960 + sigma_e * (atmosphere[h].nue * atmosphere[h].nue) / (atmosphere[h].nue*atmosphere[h].nue + e_gyroFreq*e_gyroFreq);
961 atmosphere[h].hallcoeff = -sigma_i * (atmosphere[h].nui * NO_gyroFreq) / (atmosphere[h].nui*atmosphere[h].nui + NO_gyroFreq*NO_gyroFreq)
962 + sigma_e * (atmosphere[h].nue * e_gyroFreq) / (atmosphere[h].nue*atmosphere[h].nue + e_gyroFreq*e_gyroFreq);
963
964 atmosphere[h].parallelcoeff = sigma_e;
965 }
966
967 // Energies of particles that sample the production array
968 // are logspace-distributed from 10^-1 to 10^2.3 keV
969 std::array<Real, SBC::productionNumParticleEnergies + 1> particle_energy; // In KeV
970 for (int e = 0; e < SBC::productionNumParticleEnergies; e++) {
971 // TODO: Hardcoded constants. Make parameter?
972 particle_energy[e] = pow(10.0, -1. + e * (2.3 + 1.) / (SBC::productionNumParticleEnergies - 1));
973 }
975
976 // Precalculate scattering rates
977 const Real eps_ion_keV = 0.035; // Energy required to create one ion
978 std::array<std::array<Real, numAtmosphereLevels>, SBC::productionNumParticleEnergies> scatteringRate;
979 for (int e = 0; e < SBC::productionNumParticleEnergies; e++) {
980
981 Real electronRange = 0.;
982 Real rho_R = 0.;
983 switch (ionizationModel) {
984 case Rees1963:
985 electronRange = 4.57e-5 * pow(particle_energy[e], 1.75); // kg m^-2
986 // Integrate downwards through the atmosphere to find density at depth=1
987 for (int h = numAtmosphereLevels - 1; h >= 0; h--) {
988 if (atmosphere[h].depth / electronRange > 1) {
989 rho_R = atmosphere[h].density;
990 break;
991 }
992 }
993 if (rho_R == 0.) {
994 rho_R = atmosphere[0].density;
995 }
996 break;
997 case Rees1989:
998 // From Rees, M. H. (1989), q 3.4.4
999 electronRange = 4.3e-6 + 5.36e-5 * pow(particle_energy[e], 1.67); // kg m^-2
1000 break;
1001 case SergienkoIvanov:
1002 electronRange = 1.64e-5 * pow(particle_energy[e], 1.67) * (1. + 9.48e-2 * pow(particle_energy[e], -1.57));
1003 break;
1004 case Robinson2020:
1005 case Juusola2025:
1006 case FixedSigma:
1007 // We don't need to actually do anything about the atmosphere here, and can just bail out.
1008 return;
1009 default:
1010 cerr << "(IONOSPHERE) Invalid value for Ionization model." << endl;
1011 abort();
1012 }
1013
1014 for (int h = 0; h < numAtmosphereLevels; h++) {
1015 Real lambda;
1016 Real rate = 0;
1017 switch (ionizationModel) {
1018 case Rees1963:
1019 // Rees et al 1963, eq. 1
1020 lambda = ReesIsotropicLambda(atmosphere[h].depth / electronRange);
1021 rate = particle_energy[e] / (electronRange / rho_R) / eps_ion_keV * lambda * atmosphere[h].density / integratedDensity;
1022 break;
1023 case Rees1989:
1024 // Rees 1989, eq. 3.3.7 / 3.3.8
1025 lambda = ReesIsotropicLambda(atmosphere[h].depth / electronRange);
1026 rate = particle_energy[e] * lambda * atmosphere[h].density / electronRange / eps_ion_keV;
1027 break;
1028 case SergienkoIvanov:
1029 lambda = SergienkoIvanovLambda(particle_energy[e] * 1000., atmosphere[h].depth / electronRange);
1030 rate = atmosphere[h].density / eps_ion_keV * particle_energy[e] * lambda / electronRange; // TODO: Albedo flux?
1031 break;
1032 case Robinson2020:
1033 case Juusola2025:
1034 case FixedSigma:
1035 // We don't need to actually do anything about the atmosphere here, and can just bail out.
1036 return;
1037 }
1038 scatteringRate[e][h] = max(0., rate); // m^-1
1039 }
1040 }
1041
1042 // Fill ionisation production table
1043 std::array<Real, SBC::productionNumParticleEnergies> differentialFlux; // Differential flux
1044
1045 for (int e = 0; e < productionNumAccEnergies; e++) {
1046
1047 const Real productionAccEnergyStep = (log10(productionMaxAccEnergy) - log10(productionMinAccEnergy)) / productionNumAccEnergies;
1048 Real accenergy = pow(10., productionMinAccEnergy + e * (productionAccEnergyStep)); // In KeV
1049
1050 for (int t = 0; t < productionNumTemperatures; t++) {
1051 const Real productionTemperatureStep = (log10(productionMaxTemperature) - log10(productionMinTemperature)) / productionNumTemperatures;
1052 Real tempenergy = pow(10, productionMinTemperature + t * productionTemperatureStep); // In KeV
1053
1054 for (int p = 0; p < SBC::productionNumParticleEnergies; p++) {
1055 // TODO: Kappa distribution here? Now only going for maxwellian
1056 Real energyparam = (particle_energy[p] - accenergy) / tempenergy; // = E_p / (kB T)
1057
1058 if (particle_energy[p] > accenergy) {
1059 Real deltaE = (particle_energy[p + 1] - particle_energy[p]) * 1e3 * physicalconstants::CHARGE; // dE in J
1060
1061 differentialFlux[p] = sqrt(1. / (2. * M_PI * physicalconstants::MASS_ELECTRON)) * particle_energy[p] / tempenergy / sqrt(tempenergy * 1e3 * physicalconstants::CHARGE) * deltaE *
1062 exp(-energyparam); // m / s ... multiplied with density, this yields a flux 1/m^2/s
1063 } else {
1064 differentialFlux[p] = 0;
1065 }
1066 }
1067 for (int h = 0; h < numAtmosphereLevels; h++) {
1068 productionTable[h][e][t] = 0;
1069 for (int p = 0; p < SBC::productionNumParticleEnergies; p++) {
1070 productionTable[h][e][t] += scatteringRate[p][h] * differentialFlux[p];
1071 }
1072 }
1073 }
1074 }
1075 }
1076
1079 for (uint n = 0; n < nodes.size(); n++) {
1080 nodes[n].parameters[NODE_BX] = /*SBC::*/ dipoleField(nodes[n].x[0], nodes[n].x[1], nodes[n].x[2], X, 0, X) + /*SBC::*/ BGB[0];
1081 nodes[n].parameters[NODE_BY] = /*SBC::*/ dipoleField(nodes[n].x[0], nodes[n].x[1], nodes[n].x[2], Y, 0, Y) + /*SBC::*/ BGB[1];
1082 nodes[n].parameters[NODE_BZ] = /*SBC::*/ dipoleField(nodes[n].x[0], nodes[n].x[1], nodes[n].x[2], Z, 0, Z) + /*SBC::*/ BGB[2];
1083 }
1084 }
1085
1086 /* Look up the free electron production rate in the ionosphere, given the atmospheric height index,
1087 * particle energy after the ionospheric potential drop and inflowing distribution temperature */
1088 Real SphericalTriGrid::lookupProductionValue(int heightindex, Real energy_keV, Real temperature_keV) {
1089 Real normEnergy = (log10(energy_keV) - log10(productionMinAccEnergy)) / (log10(productionMaxAccEnergy) - log10(productionMinAccEnergy));
1090 if (normEnergy < 0) {
1091 normEnergy = 0;
1092 }
1093 Real normTemperature = (log10(temperature_keV) - log10(productionMinTemperature)) / (log(productionMaxTemperature) - log(productionMinTemperature));
1094 if (normTemperature < 0) {
1095 normTemperature = 0;
1096 }
1097
1098 // Interpolation bin and parameters
1099 normEnergy *= productionNumAccEnergies;
1100 int energyindex = int(float(normEnergy));
1101 if (energyindex < 0) {
1102 energyindex = 0;
1103 normEnergy = 0;
1104 }
1105 if (energyindex > productionNumAccEnergies - 2) {
1106 energyindex = productionNumAccEnergies - 2;
1107 normEnergy = 0;
1108 }
1109 float t = normEnergy - floor(normEnergy);
1110
1111 normTemperature *= productionNumTemperatures;
1112 int temperatureindex = int(float(normTemperature));
1113 float s = normTemperature - floor(normTemperature);
1114 if (temperatureindex < 0) {
1115 temperatureindex = 0;
1116 normTemperature = 0;
1117 }
1118 if (temperatureindex > productionNumTemperatures - 2) {
1119 temperatureindex = productionNumTemperatures - 2;
1120 normTemperature = 0;
1121 }
1122
1123 // Lookup production rate by linearly interpolating table.
1124 return (productionTable[heightindex][energyindex][temperatureindex]*(1.-t) +
1125 productionTable[heightindex][energyindex+1][temperatureindex] * t) * (1.-s) +
1126 (productionTable[heightindex][energyindex][temperatureindex+1]*(1.-t) +
1127 productionTable[heightindex][energyindex+1][temperatureindex+1] * t) * s ;
1128 }
1129
1130 /* Estimate the magnetospheric electron precipitation energy flux (in W/m^2) from
1131 * mass density, electron temperature and potential difference.
1132 *
1133 * TODO: This is the coarse MHD estimate, lacking a better approximation. Should this
1134 * instead use the precipitation data reducer?
1135 */
1137
1138 for (uint n = 0; n < nodes.size(); n++) {
1139 Real ne = nodes[n].electronDensity();
1140 Real electronEnergy = nodes[n].electronTemperature() * physicalconstants::K_B;
1141 Real potential = nodes[n].deltaPhi();
1142
1143 nodes[n].parameters[ionosphereParameters::PRECIP] = (ne / sqrt(2. * M_PI * physicalconstants::MASS_ELECTRON * electronEnergy))
1144 * (2. * electronEnergy * electronEnergy + 2 * physicalconstants::CHARGE * potential * electronEnergy
1145 + (physicalconstants::CHARGE * potential)*(physicalconstants::CHARGE * potential));
1146 }
1147 }
1148
1149 /* Calculate the conductivity tensor for every grid node, based on the
1150 * given F10.7 photospheric flux as a solar activity proxy.
1151 *
1152 * This assumes the FACs have already been coupled into the grid.
1153 *
1154 * If refillTensorAtRestart is true, we don't recompute precipitation and
1155 * integration, we just refill the tensor from the sigmas as read from
1156 * restart. That is necessary so ig_inplanecurrent has non-zero data if an
1157 * output file is written after restart and before the next ionosphere
1158 * solution step.
1159 */
1160 void SphericalTriGrid::calculateConductivityTensor(const Real F10_7, const Real recombAlpha, const Real backgroundIonisation, const bool refillTensorAtRestart /*=false*/
1161 ) {
1162 phiprof::Timer timer{"ionosphere-calculateConductivityTensor"};
1163
1164 // At restart we have SIGMAP, SIGMAH and SIGMAPARALLEL read in from the restart file already, no need to update here.
1165 if (!refillTensorAtRestart) {
1166 // Ranks that don't participate in ionosphere solving skip this function outright
1168 return;
1169 }
1170
1172 if (ionosphereGrid.ionizationModel == Robinson2020) {
1173 // In the Robinson (2020) model, conductivity gets directly calculated from FACs.
1174 // DOI: doi/10.1029/2020JA028008
1175 const static std::array<Real, 3> SigmaP0d_coefficients = {5.0, -0.8, 60.9};
1176 const static std::array<Real, 3> SigmaP0u_coefficients = {4.2, 1.1, 318.6};
1177 const static std::array<Real, 3> SigmaH0d_coefficients = {7.7, -1.8, 139.0};
1178 const static std::array<Real, 3> SigmaH0u_coefficients = {8.7, 4.6, 327.1};
1179
1180 const static std::array<Real, 3> SigmaP1d_coefficients = {-3.2, -3.6, 21.9};
1181 const static std::array<Real, 3> SigmaP1u_coefficients = { 6.8, -1.5, 184.9};
1182 const static std::array<Real, 3> SigmaH1d_coefficients = {-7.3, 5.6, 100.9};
1183 const static std::array<Real, 3> SigmaH1u_coefficients = {14.8, -10.4, 129.9};
1184
1185 // MLT interpolation (eq 7 from the paper)
1186 auto interpolate_robinson = [](const std::array<Real, 3>& variable, Real MLT) -> Real {
1187 return variable[0] + variable[1] * cos(variable[2] / 180. * M_PI + MLT);
1188 };
1189
1190 // Smooth (cubic hermite) interpolation between two curves a and b, x is clamped to [-1; 1]
1191 auto smoothstep = [](Real a, Real b, Real x) -> Real {
1192 x = 0.5 * (x + 1);
1193 x = std::clamp((x - a) / (b - a), 0., 1.);
1194 x = x * x * (3 - 2 * x);
1195 return (1. - x) * a + x * b;
1196 };
1197
1198 for (uint n = 0; n < nodes.size(); n++) {
1199
1200 Real MLT = atan2(nodes[n].x[1], nodes[n].x[0]);
1201
1202 // Calculate FAC density through this node
1203 Real area = 0;
1204 for (uint e = 0; e < nodes[n].numTouchingElements; e++) {
1205 area += elementArea(nodes[n].touchingElements[e]);
1206 }
1207 area /= 3.; // As every element has 3 corners, don't double-count areas
1208
1209 // The Robinson model wants FACS in microAmperes / m^2
1210 Real FAC = 1e6 * nodes[n].parameters[ionosphereParameters::SOURCE] / area;
1211
1212 // Get A, B and C factor by interpolation
1213 // Note: Positive FAC value -> downwards FACs.
1214 Real SigmaH0 = smoothstep(interpolate_robinson(SigmaH0u_coefficients, MLT), interpolate_robinson(SigmaH0d_coefficients, MLT), FAC / 0.1);
1215 Real SigmaH1 = smoothstep(interpolate_robinson(SigmaH1u_coefficients, MLT), interpolate_robinson(SigmaH1d_coefficients, MLT), FAC / 0.1);
1216 Real SigmaP0 = smoothstep(interpolate_robinson(SigmaP0u_coefficients, MLT), interpolate_robinson(SigmaP0d_coefficients, MLT), FAC / 0.1);
1217 Real SigmaP1 = smoothstep(interpolate_robinson(SigmaP1u_coefficients, MLT), interpolate_robinson(SigmaP1d_coefficients, MLT), FAC / 0.1);
1218
1219 nodes[n].parameters[ionosphereParameters::SIGMAP] = SigmaP0 + SigmaP1 * FAC;
1220 nodes[n].parameters[ionosphereParameters::SIGMAH] = SigmaH0 + SigmaH1 * FAC;
1221 // TODO: What do we do about SIGMAPARALLEL?
1222 }
1223 } else if (ionosphereGrid.ionizationModel == Juusola2025) {
1224
1225 // Ionosoheric Sigma calculation functions from
1226 // Juusola et al. 2025.
1227 // Coefficients are in ionosphere_tables.h
1228 // Note: MLT is in hours
1229 std::function<Real(Real)> c4P = [](Real MLT) {
1230 MLT = fmod(MLT, 24.);
1231 int sector = MLT;
1232 Real interpolant = MLT - sector;
1233 return (1. - interpolant) * c4P_values[sector] + interpolant * c4P_values[(sector + 1) % 24];
1234 };
1235
1236 std::function<Real(Real)> c5P = [](Real MLT) {
1237 MLT = fmod(MLT, 24.);
1238 int sector = MLT;
1239 Real interpolant = MLT - sector;
1240 return (1. - interpolant) * c5P_values[sector] + interpolant * c5P_values[(sector + 1) % 24];
1241 };
1242
1243 std::function<Real(Real)> c4H = [](Real MLT) {
1244 MLT = fmod(MLT, 24.);
1245 int sector = MLT;
1246 Real interpolant = MLT - sector;
1247 return (1. - interpolant) * c4H_values[sector] + interpolant * c4H_values[(sector + 1) % 24];
1248 };
1249
1250 std::function<Real(Real)> c5H = [](Real MLT) {
1251 MLT = fmod(MLT, 24.);
1252 int sector = MLT;
1253 Real interpolant = MLT - sector;
1254 return (1. - interpolant) * c5H_values[sector] + interpolant * c5H_values[(sector + 1) % 24];
1255 };
1256
1257 // Eigen vector and matrix for solving
1258 // Each current-density vector lives in the circumcentre of each triangle
1259 // The constraints are calculated on each node
1260 Eigen::VectorXd vJ(2 * elements.size()); // 2 * elements.size() because we have two components of J in every element
1261 Eigen::VectorXd vRHS1(nodes.size() + nodes.size()); // Right hand side for divergence-free system
1262 Eigen::VectorXd vRHS2(nodes.size() + nodes.size()); // Right hand side for curl-free system
1263 Eigen::SparseMatrix<Real> curlSolverMatrix(vRHS1.size(), vJ.size());
1264
1265 std::vector<Real> elementCorrectionFactors(elements.size());
1266
1267 // First, solve curl-free inplane current system.
1268 // Use those currents to estimate sigma ratio.
1269 // Then, solve divergence-free part.
1270 // Finally, estimate Sigmas.
1271
1272 // This formalism uses a cirumcentre-based current-density vector field.
1273 //
1274 // To calculate the divergence at a specific node, the current density is
1275 // first interpolated to all the edges subtended by this node by weighing
1276 // the current-density of the elements subtended by a particular edge by
1277 // the proportion of the distances from the circumcentres to the midpoint
1278 // of that edge, to the line connecting the two circumcentres. This line
1279 // will always be the perpendicular bisector of the common edge, thanks to
1280 // the fact that circumcentres are equidistant from the corners of a
1281 // triangle. Then, the dot product of the edge-interpolated current
1282 // densities with the edge parallel is taken, multiplied by the length of
1283 // the dual to this edge, and summed over all edges subtended by the node.
1284 //
1285 // Since the mesh is not flat, the edge vectors are be transformed to a
1286 // common coordinate system (XY plane at the north pole) before the dot
1287 // product is taken
1288 if (Eigen::loadMarket(curlSolverMatrix, "ionosphereSolverMatrix")) {
1289
1290 for (unsigned int n = 0; n < nodes.size(); n++) {
1291 vRHS1[n] = 0;
1292 vRHS2[n] = nodes[n].parameters[ionosphereParameters::SOURCE];
1293 }
1294
1295 for (unsigned int n = 0; n < nodes.size(); n++) {
1296 vRHS1[n + nodes.size()] = ionosphereGrid.nodes[n].parameters[ionosphereParameters::SOURCE];
1297 vRHS2[n + nodes.size()] = 0;
1298 }
1299
1300 } else {
1301 // Divergence constraints
1302 for (uint gridNodeIndex = 0; gridNodeIndex < nodes.size(); gridNodeIndex++) {
1303
1304 // Divergence of divergence-free current
1305 vRHS1[gridNodeIndex] = 0;
1306
1307 // Divergence of curl-free current
1308 vRHS2[gridNodeIndex] = nodes[gridNodeIndex].parameters[ionosphereParameters::SOURCE];
1309
1310 for (uint32_t elLocalIndex = 0; elLocalIndex < nodes[gridNodeIndex].numTouchingElements; elLocalIndex++) {
1311 SphericalTriGrid::Element& element = elements[nodes[gridNodeIndex].touchingElements[elLocalIndex]];
1312
1313 // Find the two other nodes on this element
1314 int gridI = 0, gridJ = 0;
1315 int localC = 0, localI = 0, localJ = 0;
1316 for (int c = 0; c < 3; c++) {
1317 if (element.corners[c] == gridNodeIndex) {
1318 localC = c;
1319 localI = (c + 1) % 3;
1320 gridI = element.corners[localI];
1321 localJ = (c + 2) % 3;
1322 gridJ = element.corners[localJ];
1323 break;
1324 }
1325 }
1326
1327 int32_t otherElementi = findElementNeighbour(nodes[gridNodeIndex].touchingElements[elLocalIndex], localC, localI);
1328 int32_t otherElementj = findElementNeighbour(nodes[gridNodeIndex].touchingElements[elLocalIndex], localC, localJ);
1329
1330 Eigen::Vector3d circumcentrem = elementCircumcentre(nodes[gridNodeIndex].touchingElements[elLocalIndex]);
1331 Eigen::Vector3d midpointmi = commonEdgeMidpoint(nodes[gridNodeIndex].touchingElements[elLocalIndex], otherElementi);
1332 Real li = (circumcentrem - midpointmi).norm();
1333
1334 Eigen::Vector3d rm(nodes[gridNodeIndex].x.data());
1335 Eigen::Vector3d ri(nodes[gridI].x.data());
1336 Eigen::Vector3d rj(nodes[gridJ].x.data());
1337 Eigen::Vector3d edge = (ri - rm) / (ri - rm).norm();
1338
1339 Eigen::Vector3d normalm = elementNormal(nodes[gridNodeIndex].touchingElements[elLocalIndex]);
1340 Eigen::Vector3d edgem = Eigen::Quaterniond::FromTwoVectors(normalm, Eigen::Vector3d::UnitZ()).toRotationMatrix() * edge;
1341
1342 curlSolverMatrix.coeffRef(gridNodeIndex, 2 * nodes[gridNodeIndex].touchingElements[elLocalIndex]) += edgem(0) * li;
1343 curlSolverMatrix.coeffRef(gridNodeIndex, 2 * nodes[gridNodeIndex].touchingElements[elLocalIndex] + 1) += edgem(1) * li;
1344
1345 Eigen::Vector3d midpointmj = commonEdgeMidpoint(nodes[gridNodeIndex].touchingElements[elLocalIndex], otherElementj);
1346 Real lj = (circumcentrem - midpointmj).norm();
1347
1348 edge = (rj - rm) / (rj - rm).norm();
1349
1350 edgem = Eigen::Quaterniond::FromTwoVectors(normalm, Eigen::Vector3d::UnitZ()).toRotationMatrix() * edge;
1351
1352 curlSolverMatrix.coeffRef(gridNodeIndex, 2 * nodes[gridNodeIndex].touchingElements[elLocalIndex]) += edgem(0) * lj;
1353 curlSolverMatrix.coeffRef(gridNodeIndex, 2 * nodes[gridNodeIndex].touchingElements[elLocalIndex] + 1) += edgem(1) * lj;
1354 }
1355 }
1356
1357 // The curl at a specific node is calculated by taking half the dot product
1358 // of the edges opposite to the node with the current-density of the
1359 // elements subtended by the node, multiplied by a consistent orientation.
1360 // Curl constraints
1361 for (uint n = 0; n < nodes.size(); n++) {
1362
1363 // Curl of divergence-free current
1364 vRHS1[nodes.size() + n] = nodes[n].parameters[ionosphereParameters::SOURCE];
1365
1366 // Curl of curl-free current
1367 vRHS2[nodes.size() + n] = 0;
1368
1369 for (uint32_t elLocalIndex = 0; elLocalIndex < nodes[n].numTouchingElements; elLocalIndex++) {
1370 SphericalTriGrid::Element& element = elements[nodes[n].touchingElements[elLocalIndex]];
1371
1372 // Find the two other nodes on this element
1373 int gridI = 0, gridJ = 0;
1374 int localC = 0, localI = 0, localJ = 0;
1375 for (int c = 0; c < 3; c++) {
1376 if (element.corners[c] == n) {
1377 localI = (c + 1) % 3;
1378 gridI = element.corners[localI];
1379 localJ = (c + 2) % 3;
1380 gridJ = element.corners[localJ];
1381 break;
1382 }
1383 }
1384
1385 Eigen::Vector3d normal = elementNormal(nodes[n].touchingElements[elLocalIndex]);
1386 Eigen::Vector3d ri(nodes[gridI].x.data());
1387 Eigen::Vector3d rj(nodes[gridJ].x.data());
1388 Eigen::Vector3d rm(nodes[n].x.data());
1389
1390 Eigen::Vector3d edgemi = (ri - rm) / (ri - rm).norm();
1391 edgemi = Eigen::Quaterniond::FromTwoVectors(normal, Eigen::Vector3d::UnitZ()).toRotationMatrix() * edgemi;
1392
1393 Eigen::Vector3d edgemj = (rj - rm) / (rj - rm).norm();
1394 edgemj = Eigen::Quaterniond::FromTwoVectors(normal, Eigen::Vector3d::UnitZ()).toRotationMatrix() * edgemj;
1395
1396 Real orientation = edgemj.cross(edgemi).dot(normal) > 0 ? 1. : -1.;
1397
1398 Eigen::Vector3d outerEdge = orientation * (rj - ri) / (rj - ri).norm();
1399 Real outerEdgeLength = (rj - ri).norm();
1400 outerEdge = Eigen::Quaterniond::FromTwoVectors(normal, Eigen::Vector3d::UnitZ()).toRotationMatrix() * outerEdge;
1401
1402 curlSolverMatrix.coeffRef(nodes.size() + n, 2 * nodes[n].touchingElements[elLocalIndex]) += outerEdge(0) * outerEdgeLength / 2.;
1403 curlSolverMatrix.coeffRef(nodes.size() + n, 2 * nodes[n].touchingElements[elLocalIndex] + 1) += outerEdge(1) * outerEdgeLength / 2.;
1404 }
1405 }
1406
1407 Eigen::saveMarket(curlSolverMatrix, "ionosphereSolverMatrix");
1408 }
1409
1410 curlSolverMatrix.makeCompressed();
1411
1412 // Solve curl-free currents.
1413 Eigen::LeastSquaresConjugateGradient<Eigen::SparseMatrix<Real>> solver;
1414 solver.compute(curlSolverMatrix);
1415 vJ = solver.solve(vRHS2);
1416
1417 elementCurlFreeCurrent.resize(elements.size());
1418 elementDivFreeCurrent.resize(elements.size());
1419 for (uint el = 0; el < elements.size(); el++) {
1420 std::array<uint32_t, 3>& corners = elements[el].corners;
1421 Eigen::Vector3d r0(nodes[corners[0]].x.data());
1422 Eigen::Vector3d r1(nodes[corners[1]].x.data());
1423 Eigen::Vector3d r2(nodes[corners[2]].x.data());
1424
1425 Eigen::Vector3d barycentre = (r0 + r1 + r2) / 3.;
1426
1427 Eigen::Vector3d rotatedVJ = Eigen::Quaterniond::FromTwoVectors(Eigen::Vector3d::UnitZ(), barycentre.normalized()).toRotationMatrix() * Eigen::Vector3d(vJ[2 * el], vJ[2 * el + 1], 0);
1428 elementCurlFreeCurrent[el] = rotatedVJ;
1429
1430 Real MLT = atan2(barycentre[1], barycentre[0]) * 12 / M_PI + 12;
1431
1432 // Note: The coefficients want to be looked up in A/km, so we multiply by 1000
1433 Real correction = pow(c4H(MLT) / c4P(MLT) * 1000 * elementCurlFreeCurrent[el].norm(), 1. / (1. + c5P(MLT) - c5H(MLT))) / (1000 * elementCurlFreeCurrent[el].norm());
1434 elementCorrectionFactors[el] = correction;
1435 }
1436
1437 // Apply correction to RHS for divergence-free current density (vRHS1)
1438 // Interpolate from elements to nodes via proportion of dual polygon contained
1439 for (uint n = 0; n < nodes.size(); n++) {
1440
1441 Real totalA = 0;
1442 Real correction = 0;
1443
1444 for (uint32_t el = 0; el < nodes[n].numTouchingElements; el++) {
1445 Real A = areaInDualPolygon(n, nodes[n].touchingElements[el]);
1446 totalA += A;
1447 correction += elementCorrectionFactors[nodes[n].touchingElements[el]] * A;
1448 }
1449 correction /= totalA;
1450
1451 // vRHS1[nodes.size()+n] = vRHS1[nodes.size()+n]*correction;
1452 }
1453
1454 // Solve divergence-free system
1455 vJ = solver.solve(vRHS1);
1456 for (uint el = 0; el < elements.size(); el++) {
1457 std::array<uint32_t, 3>& corners = elements[el].corners;
1458
1459 Eigen::Vector3d r0(nodes[corners[0]].x.data());
1460 Eigen::Vector3d r1(nodes[corners[1]].x.data());
1461 Eigen::Vector3d r2(nodes[corners[2]].x.data());
1462
1463 Eigen::Vector3d barycentre = (r0 + r1 + r2) / 3.;
1464
1465 Eigen::Vector3d rotatedVJ = Eigen::Quaterniond::FromTwoVectors(Eigen::Vector3d::UnitZ(), barycentre.normalized()).toRotationMatrix() * Eigen::Vector3d(vJ[2 * el], vJ[2 * el + 1], 0);
1466 elementDivFreeCurrent[el] = rotatedVJ;
1467 }
1468
1469 // Next, evaluate Sigma as a function of inplane-J and MLT
1470 #pragma omp parallel for
1471 for (uint n = 0; n < nodes.size(); n++) {
1472 Eigen::Vector3d J{0, 0, 0};
1473 Eigen::Vector3d x(nodes[n].x.data());
1474
1475 Real totalA = 0;
1476 for (uint32_t el = 0; el < nodes[n].numTouchingElements; el++) {
1477 Real A = areaInDualPolygon(n, nodes[n].touchingElements[el]);
1478 totalA += A;
1479 J += elementDivFreeCurrent[nodes[n].touchingElements[el]] * A;
1480 }
1481 J /= totalA;
1482
1483 Real MLT = atan2(x[1], x[0]) * 12 / M_PI + 12;
1484
1485 // Formula 33 from Juusola et al 2025
1486 // (in A/km)
1487 J *= 1000;
1488 // cout << "J: " << J.norm() << endl;
1489 Real SigmaH = c4H(MLT) * pow(J.norm(), c5H(MLT));
1490 Real SigmaP = c4P(MLT) * pow(J.norm(), c5P(MLT));
1491
1492 nodes[n].parameters[ionosphereParameters::SIGMAP] = SigmaP;
1493 nodes[n].parameters[ionosphereParameters::SIGMAH] = SigmaH;
1494 }
1495
1496 // Perform distance transform on the mesh
1497 // Here we have, as temporary variables:
1498 // ZZPARAM -> index of closest node (so far)
1499 // PPARAM -> distance to boundary
1500 // cout << nodes[0].openFieldLine << endl;
1501 for (unsigned int n = 0; n < nodes.size(); n++) {
1502 if (nodes[n].openFieldLine == FieldTracing::TracingLineEndType::CLOSED) {
1503 nodes[n].parameters[ionosphereParameters::ZZPARAM] = n;
1504 nodes[n].parameters[ionosphereParameters::PPARAM] = 0;
1505 } else {
1506 nodes[n].parameters[ionosphereParameters::ZZPARAM] = -1;
1507 nodes[n].parameters[ionosphereParameters::PPARAM] = 6371e3;
1508 }
1509 }
1510
1511 bool done = false;
1512 while (!done) {
1513 done = true;
1514 for (unsigned int n = 0; n < nodes.size(); n++) {
1515 if (nodes[n].openFieldLine == FieldTracing::TracingLineEndType::CLOSED) {
1516 continue; // Skip closed nodes
1517 }
1518 Eigen::Vector3d x(nodes[n].x.data());
1519
1520 for (unsigned int m = 0; m < nodes[n].numTouchingElements; m++) {
1521 SphericalTriGrid::Element& element = elements[nodes[n].touchingElements[m]];
1522 for (int c = 0; c < 3; c++) {
1523 unsigned int i = element.corners[c];
1524 if (i == n) {
1525 continue;
1526 }
1527
1528 if (nodes[i].openFieldLine == FieldTracing::TracingLineEndType::CLOSED) {
1529 // Closed nodes can be probed directly
1530 Eigen::Vector3d ox(nodes[i].x.data());
1531 Real distance = (ox - x).norm();
1532 if (distance < nodes[n].parameters[ionosphereParameters::PPARAM]) {
1533 nodes[n].parameters[ionosphereParameters::PPARAM] = distance;
1534 nodes[n].parameters[ionosphereParameters::ZZPARAM] = i;
1535 done = false;
1536 }
1537 } else {
1538 // Open nodes require inferred distance
1539 // TODO: This should actually be geodetic distance, but maybe we can afford not to care
1540 if (nodes[i].parameters[ionosphereParameters::ZZPARAM] == -1) {
1541 // This node doesn't even have a distance yet, skipping.
1542 // done = false;
1543 continue;
1544 }
1545
1546 Eigen::Vector3d ox(nodes[nodes[i].parameters[ionosphereParameters::ZZPARAM]].x.data());
1547 Real distance = (ox - x).norm();
1548 if (distance < nodes[n].parameters[ionosphereParameters::PPARAM]) {
1549 nodes[n].parameters[ionosphereParameters::PPARAM] = distance;
1551 done = false;
1552 }
1553 }
1554 }
1555 }
1556 }
1557 }
1558
1559 #pragma omp parallel for
1560 for (unsigned int n = 0; n < nodes.size(); n++) {
1561
1562 // Adjust sigmas based on distance value
1563 if (nodes[n].parameters[ionosphereParameters::PPARAM] > 300e3) { // TODO: Hardcoded 300km here
1564 Real alpha = (nodes[n].parameters[ionosphereParameters::PPARAM] - 300e3) / 300e3;
1565 nodes[n].parameters[ionosphereParameters::SIGMAP] *= exp(-alpha);
1566 nodes[n].parameters[ionosphereParameters::SIGMAH] *= exp(-alpha);
1567 }
1568
1569 // Drop-in replacement for cosine function for describing plasma
1570 // production at the height of max plasma production using the
1571 // Chapman function (which assumes the earth is round, not flat).
1572 //
1573 // The advantage of this approach is that the conductance gradient at the terminator is
1574 // more realistic. This is important since conductance gradients appear in the equations that
1575 // relate electric and magnetic fields. In addition, conductances above 90° sza are positive.
1576 // The code is based on table lookup, and does not calculate the Chapman function.
1577 // Author: S. M. Hatch (2024)
1578 // (Remember to include ionosphere_tables.h for the chapman function)
1579 auto altcos = [](Real sza) -> Real {
1580 Real degrees = fabs(sza) / M_PI * 180;
1581
1582 // Clamp to table lookup range
1583 degrees = max(0., degrees);
1584 degrees = min(120., degrees);
1585
1586 int bin = degrees * 10.;
1587 Real interpolant = bin - (degrees * 10.);
1588 return (1. - interpolant) * chapman_euv_table[bin] + interpolant * chapman_euv_table[bin + 1];
1589 };
1590
1591 // Also add solar contribution
1592 // Solar incidence parameter for calculating UV ionisation on the dayside
1593 Real coschi = nodes[n].x[0] / Ionosphere::innerRadius;
1594 Real chi = acos(coschi);
1595 Real qprime = altcos(chi);
1596
1597 const Real F10_7 = 100;
1598 Real sigmaP_dayside = c1p * pow(F10_7, c2p) * pow(qprime, c3p);
1599 Real sigmaH_dayside = c1h * pow(F10_7, c2h) * pow(qprime, c3h);
1600
1601 Real SigmaP = nodes[n].parameters[ionosphereParameters::SIGMAP];
1602 Real SigmaH = nodes[n].parameters[ionosphereParameters::SIGMAH];
1603
1604 nodes[n].parameters[ionosphereParameters::SIGMAP] = sqrt(SigmaP * SigmaP + sigmaP_dayside * sigmaP_dayside + 0.625 * 0.625);
1605 nodes[n].parameters[ionosphereParameters::SIGMAH] = sqrt(SigmaH * SigmaH + sigmaH_dayside * sigmaH_dayside + 0.894 * 0.894);
1606
1607 // TODO: We could instead directly calculate element conductivities using Whitney forms
1608 // and don't need to go via sigma averaging here.
1609 // clang-format off
1610 static const int epsilon[3][3][3] = {
1611 {{0,0, 0}, { 0,0,1}, {0,-1,0}},
1612 {{0,0,-1}, { 0,0,0}, {1, 0,0}},
1613 {{0,1, 0}, {-1,0,0}, {0, 0,0}}
1614 };
1615 // clang-format on
1616
1617 Eigen::Vector3d b(nodes[n].x.data());
1618 b.normalized();
1619 if (nodes[n].x[2] >= 0) {
1620 b *= -1;
1621 }
1622 for (int i = 0; i < 3; i++) {
1623 for (int j = 0; j < 3; j++) {
1624 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] = SigmaP * (((i == j) ? 1. : 0.) - b[i] * b[j]);
1625 for (int k = 0; k < 3; k++) {
1626 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] -= SigmaH * epsilon[i][j][k] * b[k];
1627 }
1628 }
1629 }
1630 }
1631 } else if (ionosphereGrid.ionizationModel == FixedSigma) {
1632 for (uint n = 0; n < nodes.size(); n++) {
1635
1636 // Antisymmetric tensor epsilon_ijk
1637 // clang-format off
1638 static const int epsilon[3][3][3] = {
1639 {{0,0, 0}, { 0,0,1}, {0,-1,0}},
1640 {{0,0,-1}, { 0,0,0}, {1, 0,0}},
1641 {{0,1, 0}, {-1,0,0}, {0, 0,0}}
1642 };
1643 // clang-format on
1644
1645 Eigen::Vector3d b(nodes[n].x.data());
1646 b.normalized();
1647 if (nodes[n].x[2] >= 0) {
1648 b *= -1;
1649 }
1650 for (int i = 0; i < 3; i++) {
1651 for (int j = 0; j < 3; j++) {
1652 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] = SigmaP * (((i == j) ? 1. : 0.) - b[i] * b[j]);
1653 for (int k = 0; k < 3; k++) {
1654 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] -= SigmaH * epsilon[i][j][k] * b[k];
1655 }
1656 }
1657 }
1658 }
1659 } else {
1660 // The other (atmospheric precipitation and height-integration-based) models
1661 // share most of their code
1662
1663 // Calculate height-integrated conductivities and 3D electron density
1664 // TODO: effdt > 0?
1665 // (Then, ne += dt*(q - alpha*ne*abs(ne))
1666 for (uint n = 0; n < nodes.size(); n++) {
1667 nodes[n].parameters[ionosphereParameters::SIGMAP] = 0;
1668 nodes[n].parameters[ionosphereParameters::SIGMAH] = 0;
1669 nodes[n].parameters[ionosphereParameters::SIGMAPARALLEL] = 0;
1670 std::array<Real, numAtmosphereLevels> electronDensity;
1671
1672 // Note this loop counts from 1 (std::vector is zero-initialized, so electronDensity[0] = 0)
1673 for (int h = 1; h < numAtmosphereLevels; h++) {
1674 // Calculate production rate
1675 Real energy_keV = max(nodes[n].deltaPhi() / 1000., productionMinAccEnergy);
1676
1677 Real ne = nodes[n].electronDensity();
1678 Real electronTemp = nodes[n].electronTemperature();
1679 Real temperature_keV = (physicalconstants::K_B / physicalconstants::CHARGE) / 1000. * electronTemp;
1680 if (!(std::isfinite(energy_keV) && std::isfinite(temperature_keV))) {
1681 cerr << "(ionosphere) NaN or inf encountered in conductivity calculation: " << endl
1682 << " `-> DeltaPhi = " << nodes[n].deltaPhi() / 1000. << " keV" << endl
1683 << " `-> energy_keV = " << energy_keV << endl
1684 << " `-> ne = " << ne << " m^-3" << endl
1685 << " `-> electronTemp = " << electronTemp << " K" << endl;
1686 }
1687 Real qref = ne * lookupProductionValue(h, energy_keV, temperature_keV);
1688
1689 // Get equilibrium electron density
1690 electronDensity[h] = sqrt(qref / recombAlpha);
1691
1692 // Calculate conductivities
1693 Real halfdx = 1000 * 0.5 * (atmosphere[h].altitude - atmosphere[h - 1].altitude);
1694 Real halfCH = halfdx * 0.5 * (atmosphere[h - 1].hallcoeff + atmosphere[h].hallcoeff);
1695 Real halfCP = halfdx * 0.5 * (atmosphere[h - 1].pedersencoeff + atmosphere[h].pedersencoeff);
1696 Real halfCpara = halfdx * 0.5 * (atmosphere[h - 1].parallelcoeff + atmosphere[h].parallelcoeff);
1697
1698 nodes[n].parameters[ionosphereParameters::SIGMAP] += (electronDensity[h] + electronDensity[h - 1]) * halfCP;
1699 nodes[n].parameters[ionosphereParameters::SIGMAH] += (electronDensity[h] + electronDensity[h - 1]) * halfCH;
1700 nodes[n].parameters[ionosphereParameters::SIGMAPARALLEL] += (electronDensity[h] + electronDensity[h - 1]) * halfCpara;
1701 }
1702 }
1703 }
1704 }
1705
1706 // Antisymmetric tensor epsilon_ijk
1707 // clang-format off
1708 static const int epsilon[3][3][3] = {
1709 {{0,0, 0}, { 0,0,1}, {0,-1,0}},
1710 {{0,0,-1}, { 0,0,0}, {1, 0,0}},
1711 {{0,1, 0}, {-1,0,0}, {0, 0,0}}
1712 };
1713 // clang-format on
1714
1715 // Pre-transformed F10_7 values
1716 Real F10_7_p_049 = pow(F10_7, 0.49);
1717 Real F10_7_p_053 = pow(F10_7, 0.53);
1718
1719 for (uint n = 0; n < nodes.size(); n++) {
1720
1721 std::array<Real, 3>& x = nodes[n].x;
1722 // TODO: Perform coordinate transformation here?
1723
1724 // Add solar EUV ionization model on top.
1725 // (This is the calculation based on Moen&Brekke)
1726
1727 // At restart we have SIGMAP, SIGMAH and SIGMAPARALLEL read in from the restart file already.
1728 // Note: The Juusola 2005 ionization model has its own UV calculation,
1729 // which is already included at this point.
1730 if (!refillTensorAtRestart && ionosphereGrid.ionizationModel != Juusola2025) {
1731
1732 // Solar incidence parameter for calculating UV ionisation on the dayside
1733 Real coschi = x[0] / Ionosphere::innerRadius;
1734 if (coschi < 0) {
1735 coschi = 0;
1736 }
1737
1738 Real sigmaP_dayside = backgroundIonisation + F10_7_p_049 * (0.34 * coschi + 0.93 * sqrt(coschi));
1739 Real sigmaH_dayside = backgroundIonisation + F10_7_p_053 * (0.81 * coschi + 0.54 * sqrt(coschi));
1740
1741 if (ionosphereGrid.ionizationModel != FixedSigma) {
1742 nodes[n].parameters[ionosphereParameters::SIGMAP] = sqrt(pow(nodes[n].parameters[ionosphereParameters::SIGMAP], 2) + pow(sigmaP_dayside, 2));
1743 nodes[n].parameters[ionosphereParameters::SIGMAH] = sqrt(pow(nodes[n].parameters[ionosphereParameters::SIGMAH], 2) + pow(sigmaH_dayside, 2));
1744 }
1745 }
1746
1747 // Build conductivity tensor
1748 Real sigmaP = nodes[n].parameters[ionosphereParameters::SIGMAP];
1749 Real sigmaH = nodes[n].parameters[ionosphereParameters::SIGMAH];
1750 Real sigmaParallel = nodes[n].parameters[ionosphereParameters::SIGMAPARALLEL];
1751
1752 // GUMICS-Style conductivity tensor.
1753 // Approximate B vector = radial vector
1754 // SigmaP and SigmaH are both in-plane with the mesh
1755 // No longitudinal conductivity
1757 std::array<Real, 3> b = {x[0] / Ionosphere::innerRadius, x[1] / Ionosphere::innerRadius, x[2] / Ionosphere::innerRadius};
1758 if (x[2] >= 0) {
1759 b[0] *= -1;
1760 b[1] *= -1;
1761 b[2] *= -1;
1762 }
1763
1764 for (int i = 0; i < 3; i++) {
1765 for (int j = 0; j < 3; j++) {
1766 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] = sigmaP * (((i == j) ? 1. : 0.) - b[i] * b[j]);
1767 for (int k = 0; k < 3; k++) {
1768 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] -= sigmaH * epsilon[i][j][k] * b[k];
1769 }
1770 }
1771 }
1773
1775 std::array<Real, 3> b = {dipoleField(x[0], x[1], x[2], X, 0, X), dipoleField(x[0], x[1], x[2], Y, 0, Y), dipoleField(x[0], x[1], x[2], Z, 0, Z)};
1776 Real Bnorm = sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]);
1777 b[0] /= Bnorm;
1778 b[1] /= Bnorm;
1779 b[2] /= Bnorm;
1780
1781 for (int i = 0; i < 3; i++) {
1782 for (int j = 0; j < 3; j++) {
1783 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] = sigmaP * ((i == j) ? 1. : 0.) + (sigmaParallel - sigmaP) * b[i] * b[j];
1784 for (int k = 0; k < 3; k++) {
1785 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] -= sigmaH * epsilon[i][j][k] * b[k];
1786 }
1787 }
1788 }
1790
1791 std::array<Real, 3> b = {dipoleField(x[0], x[1], x[2], X, 0, X), dipoleField(x[0], x[1], x[2], Y, 0, Y), dipoleField(x[0], x[1], x[2], Z, 0, Z)};
1792 Real Bnorm = sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]);
1793 b[0] /= Bnorm;
1794 b[1] /= Bnorm;
1795 b[2] /= Bnorm;
1796
1797 for (int i = 0; i < 3; i++) {
1798 for (int j = 0; j < 3; j++) {
1799 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] = sigmaP * ((i == j) ? 1. : 0.) + (sigmaParallel - sigmaP) * b[i] * b[j];
1800 for (int k = 0; k < 3; k++) {
1801 nodes[n].parameters[ionosphereParameters::SIGMA + i * 3 + j] -= sigmaH * epsilon[i][j][k] * b[k];
1802 }
1803 }
1804 }
1805 } else {
1806 cerr << "(ionosphere) Error: Undefined conductivity model " << Ionosphere::conductivityModel << "! Ionospheric Sigma Tensor will be zero." << endl;
1807 }
1808 }
1809 }
1810
1811 // (Re-)create the subcommunicator for ionosphere-internal communication
1812 // This needs to be rerun after Vlasov grid load balancing to ensure that
1813 // ionosphere info is still communicated to the right ranks.
1814 void SphericalTriGrid::updateIonosphereCommunicator(dccrg::Dccrg<SpatialCell, dccrg::Cartesian_Geometry>& mpiGrid, fsgrids::technicalspan technical, FieldSolverGrid& fsgrid) {
1815 phiprof::Timer timer{"ionosphere-updateIonosphereCommunicator"};
1816
1817 // Check if the current rank contains ionosphere boundary cells.
1818 isCouplingOutwards = true;
1819 // for(const auto& cell: mpiGrid.get_cells()) {
1820 // if(mpiGrid[cell]->sysBoundaryFlag == sysboundarytype::IONOSPHERE) {
1821 // isCouplingOutwards = true;
1822 // }
1823 // }
1824
1825 // If a previous communicator existed, destroy it.
1826 if (communicator != MPI_COMM_NULL) {
1827 MPI_Comm_free(&communicator);
1828 communicator = MPI_COMM_NULL;
1829 }
1830
1831 // Whether or not the current rank is coupling inwards from fsgrid was determined at
1832 // grid initialization time and does not change during runtime.
1833 int writingRankInput = 0;
1835 int size;
1836 MPI_Comm_split(MPI_COMM_WORLD, 1, fsgrid.getRank(), &communicator);
1837 MPI_Comm_rank(communicator, &rank);
1838 MPI_Comm_size(communicator, &size);
1839 if (rank == 0) {
1840 writingRankInput = fsgrid.getRank();
1841 }
1842
1843 } else {
1844 MPI_Comm_split(MPI_COMM_WORLD, MPI_UNDEFINED, 0, &communicator); // All other ranks are staying out of the communicator.
1845 rank = -1;
1846 }
1847
1848 // Make sure all tasks know which task on MPI_COMM_WORLD does the writing
1849 MPI_Allreduce(&writingRankInput, &writingRank, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
1850 }
1851
1852 // Calculate upmapped potential at the given coordinates,
1853 // by tracing down to the ionosphere and interpolating the appropriate element
1855
1856 if (!this->dipoleField) {
1857 // Timestep zero => apparently the dipole field is not initialized yet.
1858 return 0.;
1859 }
1860 Real potential = 0;
1861
1862 // Do we have a stored coupling for these coordinates already?
1863 #pragma omp critical(coupling)
1864 {
1865 if (vlasovGridCoupling.find(x) == vlasovGridCoupling.end()) {
1866
1867 // If not, create one.
1869 }
1870
1871 const std::array<std::pair<int, Real>, 3>& coupling = vlasovGridCoupling[x];
1872
1873 for (int i = 0; i < 3; i++) {
1874 potential += coupling[i].second * nodes[coupling[i].first].parameters[ionosphereParameters::SOLUTION];
1875 }
1876 }
1877 return potential;
1878 }
1879
1880 // Transport field-aligned currents down from the simulation cells to the ionosphere
1882
1884 return;
1885 }
1886
1887 phiprof::Timer timer{"ionosphere-mapDownMagnetosphere"};
1888
1889 // Create zeroed-out input arrays
1890 std::vector<double> FACinput(nodes.size());
1891 std::vector<double> rhoInput(nodes.size());
1892 std::vector<double> temperatureInput(nodes.size());
1893
1894 // Map all coupled nodes down into it
1895 // Tasks that don't have anything to couple to can skip this step.
1896 if (isCouplingInwards) {
1897 #pragma omp parallel for
1898 for (uint n = 0; n < nodes.size(); n++) {
1899
1900 Real nodeAreaGeometric = 0;
1901
1902 // Map down FAC based on magnetosphere rotB
1903 if (nodes[n].xMapped[0] == 0. && nodes[n].xMapped[1] == 0. && nodes[n].xMapped[2] == 0.) {
1904 // Skip cells that couple nowhere
1905 continue;
1906 }
1907
1908 // Iterate through the elements touching that node
1909 for (uint e = 0; e < nodes[n].numTouchingElements; e++) {
1910 // Also sum up touching elements' areas and upmapped areas to compress
1911 // density and temperature with them
1912 // TODO: Precalculate this?
1913 nodeAreaGeometric += elementArea(nodes[n].touchingElements[e]);
1914 }
1915
1916 // Divide by 3, as every element will be counted from each of its
1917 // corners. Prevent areas from being multiply-counted
1918 nodeAreaGeometric /= 3.;
1919
1920 std::array<Real, 3> curlB;
1921 std::array<fsgrid::FsIndex_t,3> lfsc = getLocalFsGridCellIndexForCoord(fsgrid, nodes[n].xMapped);
1922
1923 // Local cell
1924 if(lfsc[0] == -1 || lfsc[1] == -1 || lfsc[2] == -1) {
1925 continue;
1926 }
1928 // Calc curlB, note division by DX one line down
1929 curlB = interpolateCurlB(
1930 perb,
1931 dperb,
1932 technical,
1933 fsgrid,
1934 FieldTracing::fieldTracingParameters.reconstructionCoefficientsCache,
1935 lfsc[0],lfsc[1],lfsc[2],
1936 nodes[n].xMapped
1937 );
1938 }
1940 curlB = {0,0,0};
1941 std::vector<std::array<double, 3>> sample_pts;
1942 std::vector<double> weights;
1943 std::vector<std::array<fsgrid::FsIndex_t,3>> lfscs;
1944
1945 double weightsum = 0.0;
1946 std::array<double, 3> gridSpacing = fsgrid.getGridSpacing();
1947 double h = Ionosphere::downmapSamplingWidth/2.0;
1948 for (int x = -1; x < 2; ++x){
1949 for (int y = -1; y < 2; ++y){
1950 for (int z = -1; z < 2; ++z){
1951 std::array<double, 3> pt = {nodes[n].xMapped[0] + x*h*gridSpacing[0],
1952 nodes[n].xMapped[1] + y*h*gridSpacing[1],
1953 nodes[n].xMapped[2] + z*h*gridSpacing[2]};
1954 std::array<fsgrid::FsIndex_t,3> lfsc_stencil = getLocalFsGridCellIndexForCoord(fsgrid, pt);
1955 if(lfsc_stencil[0] == -1 || lfsc_stencil[1] == -1 || lfsc_stencil[2] == -1) {
1956 continue;
1957 }
1958 sample_pts.push_back(pt);
1959 lfscs.push_back(lfsc_stencil);
1960 weights.push_back(1.0);
1961 weightsum+=weights.back();
1962 }
1963 }
1964 }
1965 // extra safety, should be covered anyway before
1966 if (sample_pts.size()==0){
1967 continue;
1968 }
1969 for (unsigned int i = 0; i < weights.size(); ++i){
1970 std::array<Real, 3> curlB_temp = interpolateCurlB(
1971 perb,
1972 dperb,
1973 technical,
1974 fsgrid,
1975 FieldTracing::fieldTracingParameters.reconstructionCoefficientsCache,
1976 lfscs[i][0],lfscs[i][1],lfscs[i][2],
1977 sample_pts[i]
1978 );
1979 for(int ii = 0; ii<3; ++ii){
1980 curlB[ii] += curlB_temp[ii]*weights[i]/weightsum;
1981 }
1982 }
1983 }
1984 else{
1985 cerr << "(IONOSPHERE) Unknown FAC sampling mode \"" << Ionosphere::downmapFACsamplingMode << "\". Aborting." << endl;
1986 abort();
1987 }
1988
1989 // Dot curl(B) with normalized B, scale by ratio of B(ionosphere)/B(upmapped),
1990 // multiply by geometric area around ionosphere node to obtain current from density
1991 FACinput[n] =
1992 nodeAreaGeometric * (nodes[n].parameters[ionosphereParameters::UPMAPPED_BX] * curlB[0] +
1993 nodes[n].parameters[ionosphereParameters::UPMAPPED_BY] * curlB[1] +
1994 nodes[n].parameters[ionosphereParameters::UPMAPPED_BZ] * curlB[2])
2001 * physicalconstants::MU_0 * fsgrid.getGridSpacing()[0]);
2002
2003 // By definition, a downwards current into the ionosphere has a positive FAC value,
2004 // as it corresponds to positive divergence of horizontal current in the ionospheric plane.
2005 // To make sure we match that, flip FAC sign on the southern hemisphere
2006 if (nodes[n].x[2] < 0) {
2007 FACinput[n] *= -1;
2008 }
2009
2010 std::array<Real, 3> frac = getFractionalFsGridCellForCoord(fsgrid, nodes[n].xMapped);
2011 for (int c = 0; c < 3; c++) {
2012 // Shift by half a cell, as we are sampling volume quantities that are logically located at cell centres.
2013 if (frac[c] < 0.5) {
2014 lfsc[c] -= 1;
2015 frac[c] += 0.5;
2016 } else {
2017 frac[c] -= 0.5;
2018 }
2019 }
2020
2021 // Linearly interpolate neighbourhood
2022 const auto stencil = fsgrid.makeStencil(lfsc[0], lfsc[1], lfsc[2]);
2023 Real couplingSum = 0;
2024 for (int xoffset : {0, 1}) {
2025 for (int yoffset : {0, 1}) {
2026 for (int zoffset : {0, 1}) {
2027 const auto index = stencil.indexFromOffset(xoffset, yoffset, zoffset);
2028
2029 Real coupling = (1. - abs(xoffset - frac[0])) * (1. - abs(yoffset - frac[1])) * (1. - abs(zoffset - frac[2]));
2030 if (coupling < 0. || coupling > 1.) {
2031 cerr << "Ionosphere warning: node << " << n << " has coupling value " << coupling << ", which is outside [0,1] at line " << __LINE__ << "!" << endl;
2032 }
2033
2034 // Only couple to actual simulation cells
2035 if (technical[index].sysBoundaryFlag == sysboundarytype::NOT_SYSBOUNDARY) {
2036 couplingSum += coupling;
2037 } else {
2038 continue;
2039 }
2040
2041 // Map density, temperature down
2042 Real thisCellRho = moments[index].at(fsgrids::RHOQ) / physicalconstants::CHARGE;
2043 rhoInput[n] += coupling * thisCellRho;
2044 temperatureInput[n] += coupling * 1. / 3. * (moments[index][fsgrids::P_11] + moments[index][fsgrids::P_22] + moments[index][fsgrids::P_33]) / (thisCellRho * physicalconstants::K_B * ion_electron_T_ratio);
2045 }
2046 }
2047 }
2048
2049 // The coupling values *would* have summed to 1 in free and open space, but since we are close to the inner
2050 // boundary, some cells were skipped, as they are in the sysbondary. Renormalize values by dividing by the
2051 // couplingSum.
2052 if (couplingSum > 0) {
2053 rhoInput[n] /= couplingSum;
2054 temperatureInput[n] /= couplingSum;
2055 }
2056 }
2057 }
2058
2059 // Allreduce on the ionosphere communicator
2060 std::vector<double> FACsum(nodes.size());
2061 std::vector<double> rhoSum(nodes.size());
2062 std::vector<double> temperatureSum(nodes.size());
2063 MPI_Allreduce(&FACinput[0], &FACsum[0], nodes.size(), MPI_DOUBLE, MPI_SUM, communicator);
2064 MPI_Allreduce(&rhoInput[0], &rhoSum[0], nodes.size(), MPI_DOUBLE, MPI_SUM, communicator);
2065 // TODO: Does it make sense to SUM the temperatures?
2066 MPI_Allreduce(&temperatureInput[0], &temperatureSum[0], nodes.size(), MPI_DOUBLE, MPI_SUM, communicator);
2067
2068 for (uint n = 0; n < nodes.size(); n++) {
2069
2070 // Adjust densities by the loss-cone filling factor.
2071 // This is an empirical smooothstep function that artificially reduces
2072 // downmapped density below auroral latitudes.
2073 Real theta = acos(nodes[n].x[2] / sqrt(nodes[n].x[0] * nodes[n].x[0] + nodes[n].x[1] * nodes[n].x[1] + nodes[n].x[2] * nodes[n].x[2])); // Latitude
2074 if (theta > M_PI / 2.) {
2075 theta = M_PI - theta;
2076 }
2077 // Smoothstep with an edge at about 67 deg.
2078 Real Chi0 = 0.01 + 0.99 * .5 * (1 + tanh((23. - theta * (180. / M_PI)) / 6));
2079
2080 if (rhoSum[n] == 0 || temperatureSum[n] == 0) {
2081 // Node couples nowhere. Assume some default values.
2082 nodes[n].parameters[ionosphereParameters::SOURCE] = 0;
2083
2086 } else {
2087 // Store as the node's parameter values.
2089 // Immediate coupling
2090 nodes[n].parameters[ionosphereParameters::SOURCE] = FACsum[n];
2091 nodes[n].parameters[ionosphereParameters::RHON] = rhoSum[n] * Chi0;
2092 nodes[n].parameters[ionosphereParameters::TEMPERATURE] = temperatureSum[n];
2093 } else {
2094
2095 // Slow coupling with a given timescale.
2096 // See https://en.wikipedia.org/wiki/Exponential_smoothing#Time_constant
2097 // P::dt valid for shorter coupling periods or Ionosphere::couplingInterval == 0 meaning every step
2098 Real timeInterval = Parameters::dt;
2100 timeInterval = Ionosphere::couplingInterval;
2101 }
2102 Real a = 1. - exp(-timeInterval / Ionosphere::couplingTimescale);
2103 if (a > 1) {
2104 a = 1.;
2105 }
2106
2107 nodes[n].parameters[ionosphereParameters::SOURCE] = (1. - a) * nodes[n].parameters[ionosphereParameters::SOURCE] + a * FACsum[n];
2108 nodes[n].parameters[ionosphereParameters::RHON] = (1. - a) * nodes[n].parameters[ionosphereParameters::RHON] + a * rhoSum[n] * Chi0;
2109 nodes[n].parameters[ionosphereParameters::TEMPERATURE] = (1. - a) * nodes[n].parameters[ionosphereParameters::TEMPERATURE] + a * temperatureSum[n];
2110 }
2111 }
2112 }
2113
2114 // Make sure FACs are balanced, so that the potential doesn't start to drift
2115 offset_FAC();
2116 }
2117
2118 // Calculate grad(T) for a element basis function that is zero at corners a and b,
2119 // and unity at corner c
2120 std::array<Real, 3> SphericalTriGrid::computeGradT(const std::array<Real, 3>& a, const std::array<Real, 3>& b, const std::array<Real, 3>& c) {
2121
2122 Vec3d av(a[0], a[1], a[2]);
2123 Vec3d bv(b[0], b[1], b[2]);
2124 Vec3d cv(c[0], c[1], c[2]);
2125
2126 Vec3d z = cross_product(bv - cv, av - cv);
2127
2128 Vec3d result = cross_product(z, bv - av) / dot_product(z, cross_product(av, bv) + cross_product(cv, av - bv));
2129
2130 return std::array<Real, 3>{result[0], result[1], result[2]};
2131 }
2132
2133 // Calculate the average sigma tensor of an element by averaging over the three nodes it touches
2134 std::array<Real, 9> SphericalTriGrid::sigmaAverage(uint elementIndex) {
2135
2136 std::array<Real, 9> retval{0, 0, 0, 0, 0, 0, 0, 0, 0};
2137
2138 for (int corner = 0; corner < 3; corner++) {
2139 Node& n = nodes[elements[elementIndex].corners[corner]];
2140 for (int i = 0; i < 9; i++) {
2141 retval[i] += n.parameters[ionosphereParameters::SIGMA + i] / 3.;
2142 }
2143 }
2144
2145 return retval;
2146 }
2147
2148 // calculate integral( grd(Ti) Sigma grad(Tj) ) over the area of the given element
2149 // The i and j parameters enumerate the piecewise linear element basis function
2150 Real SphericalTriGrid::elementIntegral(uint elementIndex, int i, int j, bool transpose) {
2151
2152 Element& e = elements[elementIndex];
2153 const std::array<Real, 3>& c1 = nodes[e.corners[0]].x;
2154 const std::array<Real, 3>& c2 = nodes[e.corners[1]].x;
2155 const std::array<Real, 3>& c3 = nodes[e.corners[2]].x;
2156
2157 std::array<Real, 3> Ti, Tj;
2158 switch (i) {
2159 case 0:
2160 Ti = computeGradT(c2, c3, c1);
2161 break;
2162 case 1:
2163 Ti = computeGradT(c1, c3, c2);
2164 break;
2165 case 2:
2166 default:
2167 Ti = computeGradT(c1, c2, c3);
2168 break;
2169 }
2170 switch (j) {
2171 case 0:
2172 Tj = computeGradT(c2, c3, c1);
2173 break;
2174 case 1:
2175 Tj = computeGradT(c1, c3, c2);
2176 break;
2177 case 2:
2178 default:
2179 Tj = computeGradT(c1, c2, c3);
2180 break;
2181 }
2182
2183 std::array<Real, 9> sigma = sigmaAverage(elementIndex);
2184
2185 Real retval = 0;
2186 if (transpose) {
2187 for (int n = 0; n < 3; n++) {
2188 for (int m = 0; m < 3; m++) {
2189 retval += Ti[m] * sigma[3 * n + m] * Tj[n];
2190 }
2191 }
2192 } else {
2193 for (int n = 0; n < 3; n++) {
2194 for (int m = 0; m < 3; m++) {
2195 retval += Ti[n] * sigma[3 * n + m] * Tj[m];
2196 }
2197 }
2198 }
2199
2200 return retval * elementArea(elementIndex);
2201 }
2202
2203 // Add matrix value for the solver, linking two nodes.
2204 void SphericalTriGrid::addMatrixDependency(uint node1, uint node2, Real coeff, bool transposed) {
2205
2206 // No need to bother with zero coupling
2207 if (coeff == 0) {
2208 return;
2209 }
2210
2211 // Special case handling for Gauge fixing. Gauge-fixed nodes only couple to themselves.
2212 if (ionosphereGrid.gaugeFixing == Pole) {
2213 if ((!transposed && node1 == 0) || (transposed && node2 == 0)) {
2214 if (node1 == node2) {
2215 coeff = 1;
2216 } else {
2217 return;
2218 }
2219 }
2220 } else if (ionosphereGrid.gaugeFixing == Equator) {
2221 // note that the second term in the first and second line involve different nodes!
2222 if ((!transposed && fabs(nodes[node1].x[2]) < Ionosphere::innerRadius * sin(Ionosphere::shieldingLatitude * M_PI / 180.0)) ||
2223 (transposed && fabs(nodes[node2].x[2]) < Ionosphere::innerRadius * sin(Ionosphere::shieldingLatitude * M_PI / 180.0))) {
2224 if (node1 == node2) {
2225 coeff = 1;
2226 } else {
2227 return;
2228 }
2229 }
2230 }
2231
2232 Node& n = nodes[node1];
2233 // First check if the dependency already exists
2234 for (uint i = 0; i < n.numDepNodes; i++) {
2235 if (n.dependingNodes[i] == node2) {
2236
2237 // Yup, found it, let's simply add the coefficient.
2238 if (transposed) {
2239 n.transposedCoeffs[i] += coeff;
2240 } else {
2241 n.dependingCoeffs[i] += coeff;
2242 }
2243 return;
2244 }
2245 }
2246
2247 // Not found, let's add it.
2248 if (n.numDepNodes >= MAX_DEPENDING_NODES - 1) {
2249 // This shouldn't happen (but did in tests!)
2250 cerr << "(ionosphere) Node " << node1 << " already has " << MAX_DEPENDING_NODES << " depending nodes:" << endl;
2251 cerr << " [ ";
2252 for (int i = 0; i < MAX_DEPENDING_NODES; i++) {
2253 cerr << n.dependingNodes[i] << ", ";
2254 }
2255 cerr << " ]." << endl;
2256
2257 std::set<uint> neighbourNodes;
2258 for (uint e = 0; e < nodes[node1].numTouchingElements; e++) {
2259 Element& E = elements[nodes[node1].touchingElements[e]];
2260 for (int c = 0; c < 3; c++) {
2261 neighbourNodes.emplace(E.corners[c]);
2262 }
2263 }
2264 cerr << " (it has " << nodes[node1].numTouchingElements << " neighbour elements and " << neighbourNodes.size() - 1 << " direct neighbour nodes:" << endl << " [ ";
2265 for (auto& n : neighbourNodes) {
2266 if (n != node1) {
2267 cerr << n << ", ";
2268 }
2269 }
2270 cerr << "])." << endl;
2271 return;
2272 }
2273 n.dependingNodes[n.numDepNodes] = node2;
2274 if (transposed) {
2275 n.dependingCoeffs[n.numDepNodes] = 0;
2276 n.transposedCoeffs[n.numDepNodes] = coeff;
2277 } else {
2278 n.dependingCoeffs[n.numDepNodes] = coeff;
2279 n.transposedCoeffs[n.numDepNodes] = 0;
2280 }
2281 n.numDepNodes++;
2282 }
2283
2284 // Add solver matrix dependencies for the neighbouring nodes
2286
2287 nodes[nodeIndex].numDepNodes = 1;
2288
2289 // Add selfcoupling dependency already, to guarantee that it sits at index 0
2290 nodes[nodeIndex].dependingNodes[0] = nodeIndex;
2291 nodes[nodeIndex].dependingCoeffs[0] = 0;
2292 nodes[nodeIndex].transposedCoeffs[0] = 0;
2293
2294 for (uint t = 0; t < nodes[nodeIndex].numTouchingElements; t++) {
2295 int j0 = -1;
2296 Element& e = elements[nodes[nodeIndex].touchingElements[t]];
2297
2298 // Find the corner this node is touching
2299 for (int c = 0; c < 3; c++) {
2300 if (e.corners[c] == nodeIndex) {
2301 j0 = c;
2302 }
2303 }
2304
2305 // Special case: we are touching the middle of an edge
2306 if (j0 == -1) {
2307 // This is not implemented. Instead, refinement interfaces are stitched, so these kinds
2308 // of T-junctions never appear.
2309 } else {
2310
2311 // Normal case.
2312 for (int c = 0; c < 3; c++) {
2313 uint neigh = e.corners[c];
2314 addMatrixDependency(nodeIndex, neigh, elementIntegral(nodes[nodeIndex].touchingElements[t], j0, c));
2315 addMatrixDependency(nodeIndex, neigh, elementIntegral(nodes[nodeIndex].touchingElements[t], j0, c, true), true);
2316 }
2317 }
2318 }
2319 }
2320
2321 // Make sure refinement interfaces are properly "stitched", and that there are no
2322 // nodes remaining on t-junctions. This is done by splitting the bigger neighbour:
2323 //
2324 // A---------------C A---------------C
2325 // / \ / / \ resized .-'/
2326 // / \ / / \ .-' /
2327 // / \ / / \ .-' /
2328 // o-------n / ==> o-------n' new /. <- potential other node to update next?
2329 // \ / \ / \ / \ / .
2330 // \ / \ / \ / \ / .
2331 // \ / \ / \ / \ / .
2332 // o-------B o-------B . . . .
2334
2335 for (uint n = 0; n < nodes.size(); n++) {
2336
2337 for (uint t = 0; t < nodes[n].numTouchingElements; t++) {
2338 Element& e = elements[nodes[n].touchingElements[t]];
2339 int j0 = -1;
2340
2341 // Find the corner this node is touching
2342 for (int c = 0; c < 3; c++) {
2343 if (e.corners[c] == n) {
2344 j0 = c;
2345 }
2346 }
2347
2348 if (j0 != -1) {
2349 // Normal element corner
2350 continue;
2351 }
2352
2353 // Not a corner of this element => Split element
2354
2355 // Find the corners of this element that we are collinear with
2356 uint A = 0, B = 0, C = 0;
2357 Real bestColinearity = 0;
2358 for (int c = 0; c < 3; c++) {
2359 Node& a = nodes[e.corners[c]];
2360 Node& b = nodes[e.corners[(c + 1) % 3]];
2361 Vec3d ab(b.x[0] - a.x[0], b.x[1] - a.x[1], b.x[2] - a.x[2]);
2362 Vec3d an(nodes[n].x[0] - a.x[0], nodes[n].x[1] - a.x[1], nodes[n].x[2] - a.x[2]);
2363
2364 Real dotproduct = dot_product(normalize_vector(ab), normalize_vector(an));
2365 if (dotproduct > 0.9 && dotproduct > bestColinearity) {
2366 A = e.corners[c];
2367 B = e.corners[(c + 1) % 3];
2368 C = e.corners[(c + 2) % 3];
2369 bestColinearity = dotproduct;
2370 }
2371 }
2372
2373 if (bestColinearity == 0) {
2374 cerr << "(ionosphere) Stitiching refinement boundaries failed: Element " << nodes[n].touchingElements[t] << " does not contain node " << n << " as a corner, yet matching edge not found." << endl;
2375 continue;
2376 }
2377
2378 // We form two elements: AnC and nBC from the old element ABC
2379 if (A == n || B == n || C == n) {
2380 cerr << "(ionosphere) ERROR: Trying to split an element at a node that is already it's corner" << endl;
2381 }
2382
2383 // Real oldArea = elementArea(nodes[n].touchingElements[t]);
2384 // Old element modified
2385 e.corners = {A, n, C};
2386 // Real newArea1 = elementArea(nodes[n].touchingElements[t]);
2387 // New element
2388 Element newElement;
2389 newElement.corners = {n, B, C};
2390
2391 uint ne = elements.size();
2392 elements.push_back(newElement);
2393 // Real newArea2 = elementArea(ne);
2394
2395 // Fix touching element lists:
2396 // Far corner touches both elements
2397 nodes[C].touchingElements[nodes[C].numTouchingElements++] = ne;
2398 if (nodes[C].numTouchingElements > MAX_TOUCHING_ELEMENTS) {
2399 cerr << "(ionosphere) ERROR: node " << C << "'s numTouchingElements (" << nodes[C].numTouchingElements << ") exceeds MAX_TOUCHING_ELEMENTS (= " << MAX_TOUCHING_ELEMENTS << ")" << endl;
2400 }
2401
2402 // Our own node too.
2403 nodes[n].touchingElements[nodes[n].numTouchingElements++] = ne;
2404 if (nodes[n].numTouchingElements > MAX_TOUCHING_ELEMENTS) {
2405 cerr << "(ionosphere) ERROR: node " << n << "'s numTouchingElements [" << nodes[n].numTouchingElements << "] exceeds MAX_TOUCHING_ELEMENTS (= " << MAX_TOUCHING_ELEMENTS << ")" << endl;
2406 }
2407
2408 // One node has been shifted to the other element. Find the old one and change it.
2409 Node& neighbour = nodes[B];
2410 for (uint i = 0; i < neighbour.numTouchingElements; i++) {
2411 if (neighbour.touchingElements[i] == nodes[n].touchingElements[t]) {
2412 neighbour.touchingElements[i] = ne;
2413 continue;
2414 }
2415
2416 // Also it's neighbour element nodes might now need their element information updated, if they sit on the B-C line
2417 Vec3d bc(nodes[C].x[0] - nodes[B].x[0], nodes[C].x[1] - nodes[B].x[1], nodes[C].x[2] - nodes[B].x[2]);
2418 for (int c = 0; c < 3; c++) {
2419 uint nn = elements[neighbour.touchingElements[i]].corners[c];
2420 if (nn == A || nn == B || nn == C || nn == n) {
2421 // Skip our own nodes
2422 continue;
2423 }
2424
2425 Vec3d bn(nodes[nn].x[0] - nodes[B].x[0], nodes[nn].x[1] - nodes[B].x[1], nodes[nn].x[2] - nodes[B].x[2]);
2426 if (dot_product(normalize_vector(bc), normalize_vector(bn)) > 0.9) {
2427 for (uint j = 0; j < nodes[nn].numTouchingElements; j++) {
2428 if (nodes[nn].touchingElements[j] == nodes[n].touchingElements[t]) {
2429 nodes[nn].touchingElements[j] = ne;
2430 continue;
2431 }
2432 }
2433 }
2434 }
2435
2436 // TODO: What about cases where we refine more than one level at once?
2437 }
2438 }
2439 }
2440 }
2441
2442 // Initialize the CG sover by assigning matrix dependency weights
2443 void SphericalTriGrid::initSolver(bool zeroOut) {
2444
2445 phiprof::Timer timer{"ionosphere-initSolver"};
2446 // Zero out parameters
2447 if (zeroOut) {
2448 for (uint n = 0; n < nodes.size(); n++) {
2450 Node& N = nodes[n];
2451 N.parameters[p] = 0;
2452 }
2453 }
2454 } else {
2455 // Only zero the gradient states
2456 Real potentialSum = 0;
2457 for (uint n = 0; n < nodes.size(); n++) {
2458 Node& N = nodes[n];
2459 potentialSum += N.parameters[ionosphereParameters::SOLUTION];
2461 N.parameters[p] = 0;
2462 }
2463 }
2464
2465 potentialSum /= nodes.size();
2466 // One option for gauge fixing:
2467 // Make sure the potential is symmetric around 0 (to prevent it from drifting)
2468 // for(uint n=0; n<nodes.size(); n++) {
2469 // Node& N=nodes[n];
2470 // N.parameters[ionosphereParameters::SOLUTION] -= potentialSum;
2471 //}
2472 }
2473
2474 #pragma omp parallel for
2475 for (uint n = 0; n < nodes.size(); n++) {
2477 }
2478
2479 // cerr << "(ionosphere) Solver dependency matrix: " << endl;
2480 // for(uint n=0; n<nodes.size(); n++) {
2481 // for(uint m=0; m<nodes.size(); m++) {
2482
2483 // Real val=0;
2484 // for(int d=0; d<nodes[n].numDepNodes; d++) {
2485 // if(nodes[n].dependingNodes[d] == m) {
2486 // val=nodes[n].dependingCoeffs[d];
2487 // }
2488 // }
2489
2490 // cerr << val << "\t";
2491 // }
2492 // cerr << endl;
2493 //}
2494 }
2495
2496 // Evaluate a nodes' neighbour parameter, averaged through the coupling
2497 // matrix
2498 //
2499 // -> "A times parameter"
2500 iSolverReal SphericalTriGrid::Atimes(uint nodeIndex, int parameter, bool transpose) {
2501 iSolverReal retval = 0;
2502 Node& n = nodes[nodeIndex];
2503
2504 if (transpose) {
2505 for (uint i = 0; i < n.numDepNodes; i++) {
2506 retval += nodes[n.dependingNodes[i]].parameters[parameter] * n.transposedCoeffs[i];
2507 }
2508 } else {
2509 for (uint i = 0; i < n.numDepNodes; i++) {
2510 retval += nodes[n.dependingNodes[i]].parameters[parameter] * n.dependingCoeffs[i];
2511 }
2512 }
2513
2514 return retval;
2515 }
2516
2517 // Evaluate a nodes' own parameter value
2518 // (If preconditioning is used, this is already adjusted for self-coupling)
2519 Real SphericalTriGrid::Asolve(uint nodeIndex, int parameter, bool transpose) {
2520
2521 Node& n = nodes[nodeIndex];
2522
2524 // Find this nodes' selfcoupling coefficient
2525 if (transpose) {
2526 return n.parameters[parameter] / n.transposedCoeffs[0];
2527 } else {
2528 return n.parameters[parameter] / n.dependingCoeffs[0];
2529 }
2530 } else {
2531 return n.parameters[parameter];
2532 }
2533 }
2534
2535 // Solve the ionosphere potential using a conjugate gradient solver
2536 void SphericalTriGrid::solve(int& nIterations, int& nRestarts, Real& residual, Real& minPotentialN, Real& maxPotentialN, Real& minPotentialS, Real& maxPotentialS) {
2537
2538 // Simulations without an ionosphere don't need to bother about this.
2539 if (nodes.size() == 0) {
2540 nIterations = 0;
2541 nRestarts = 0;
2542 residual = 0.;
2543 minPotentialN = maxPotentialN = minPotentialS = maxPotentialS = 0.;
2544 return;
2545 }
2546
2547 // Ranks that don't participate in ionosphere solving skip this function outright
2549 return;
2550 }
2551
2552 phiprof::Timer timer{"ionosphere-solve"};
2553
2554 initSolver(false);
2555
2556 nIterations = 0;
2557 nRestarts = 0;
2558
2559 for (uint n = 0; n < nodes.size(); n++) {
2560
2562
2563 if (fabs(N.x[2]) < Ionosphere::innerRadius * sin(Ionosphere::shieldingLatitude * M_PI / 180.0)) {
2565 }
2566 }
2567
2569 // BiCGSTAB solver from Eigen
2570 Eigen::SparseMatrix<Real> potentialSolverMatrix(nodes.size(), nodes.size());
2571 Eigen::VectorXd vRightHand(nodes.size()), vPhi(nodes.size());
2572
2573 for (uint n = 0; n < nodes.size(); n++) {
2574 for (uint m = 0; m < nodes[n].numDepNodes; m++) {
2575 potentialSolverMatrix.insert(n, nodes[n].dependingNodes[m]) = nodes[n].dependingCoeffs[m];
2576 }
2577 vRightHand[n] = nodes[n].parameters[ionosphereParameters::SOURCE];
2578 }
2579
2580 potentialSolverMatrix.makeCompressed();
2581
2582 Eigen::BiCGSTAB<Eigen::SparseMatrix<Real>> solver;
2583
2584 solver.compute(potentialSolverMatrix);
2585
2586 vPhi = solver.solve(vRightHand);
2587
2588 for (uint n = 0; n < nodes.size(); n++) {
2589 nodes[n].parameters[ionosphereParameters::SOLUTION] = vPhi[n];
2590 }
2591
2592 nIterations = solver.iterations();
2593
2594 residual = solver.error();
2595
2596 for (uint n = 0; n < nodes.size(); n++) {
2597
2598 Node& N = nodes[n];
2599
2600 if (N.x[2] >= 0) {
2601 if (N.parameters.at(ionosphereParameters::SOLUTION) < minPotentialN) {
2602 minPotentialN = N.parameters.at(ionosphereParameters::SOLUTION);
2603 }
2604 if (N.parameters.at(ionosphereParameters::SOLUTION) > maxPotentialN) {
2605 maxPotentialN = N.parameters.at(ionosphereParameters::SOLUTION);
2606 }
2607 } else {
2608 if (N.parameters.at(ionosphereParameters::SOLUTION) < minPotentialS) {
2609 minPotentialS = N.parameters.at(ionosphereParameters::SOLUTION);
2610 }
2611 if (N.parameters.at(ionosphereParameters::SOLUTION) > maxPotentialS) {
2612 maxPotentialS = N.parameters.at(ionosphereParameters::SOLUTION);
2613 }
2614 }
2615 }
2616 } else {
2617 // Our own BiCG implementation in solveInternal, like in GUMICS' implementation.
2618 do {
2619 solveInternal(nIterations, nRestarts, residual, minPotentialN, maxPotentialN, minPotentialS, maxPotentialS);
2622 }
2624 }
2625 }
2626
2627 void SphericalTriGrid::solveInternal(int& iteration, int& nRestarts, Real& minerr, Real& minPotentialN, Real& maxPotentialN, Real& minPotentialS, Real& maxPotentialS) {
2628 std::vector<iSolverReal> effectiveSource(nodes.size());
2629
2630 // for loop reduction variables, declared before omp parallel region
2631 iSolverReal akden;
2632 iSolverReal bknum;
2633 iSolverReal potentialInt;
2634 iSolverReal sourcenorm;
2635 iSolverReal residualnorm;
2636 minPotentialN = minPotentialS = std::numeric_limits<iSolverReal>::max();
2637 maxPotentialN = maxPotentialS = std::numeric_limits<iSolverReal>::lowest();
2638#ifdef IONOSPHERE_SORTED_SUMS
2639 std::multiset<iSolverReal> set_neg, set_pos;
2640#endif
2641
2642#ifdef IONOSPHERE_SORTED_SUMS
2643 #pragma omp parallel shared(akden, bknum, potentialInt, sourcenorm, residualnorm, effectiveSource, minPotentialN, maxPotentialN, minPotentialS, maxPotentialS, set_neg, set_pos)
2644#else
2645 #pragma omp parallel shared(akden, bknum, potentialInt, sourcenorm, residualnorm, effectiveSource, minPotentialN, maxPotentialN, minPotentialS, maxPotentialS)
2646#endif
2647 {
2648
2649 // thread variables, initialised here
2650 iSolverReal err = 0;
2651 iSolverReal thread_minerr = std::numeric_limits<iSolverReal>::max();
2652 int thread_iteration = iteration;
2653 int thread_nRestarts = nRestarts;
2654#ifdef IONOSPHERE_SORTED_SUMS
2655 std::multiset<iSolverReal> thread_set_neg, thread_set_pos;
2656#endif
2657
2658 iSolverReal bkden = 1;
2659 int failcount = 0;
2660 int counter = 0;
2661
2662 #pragma omp single
2663 { sourcenorm = 0; }
2664 // Calculate sourcenorm and initial residual estimate
2665#ifdef IONOSPHERE_SORTED_SUMS
2666 #pragma omp for
2667#else
2668 #pragma omp for reduction(+ : sourcenorm)
2669#endif
2670 for (uint n = 0; n < nodes.size(); n++) {
2671 Node& N = nodes[n];
2672 // Set gauge-pinned nodes to their fixed potential
2673 // if(ionosphereGrid.gaugeFixing == Pole && n == 0) {
2674 // effectiveSource[n] = 0;
2675 //} else if(ionosphereGrid.gaugeFixing == Equator && fabs(N.x[2]) < Ionosphere::innerRadius * sin(Ionosphere::shieldingLatitude * M_PI / 180.0)) {
2676 // effectiveSource[n] = 0;
2677 //} else {
2679 effectiveSource[n] = source;
2680 //}
2681 if (source != 0) {
2682#ifdef IONOSPHERE_SORTED_SUMS
2683 thread_set_pos.insert(source * source);
2684#else
2685 sourcenorm += source * source;
2686#endif
2687 }
2692 } else {
2694 }
2695 }
2696#ifdef IONOSPHERE_SORTED_SUMS
2697 #pragma omp critical
2698 { set_pos.insert(thread_set_pos.begin(), thread_set_pos.end()); }
2699#endif
2700 #pragma omp barrier
2701 #pragma omp single
2702 {
2703#ifdef IONOSPHERE_SORTED_SUMS
2704 for (auto it = set_pos.crbegin(); it != set_pos.crend(); it++) {
2705 sourcenorm += *it;
2706 }
2707#endif
2708 sourcenorm = sqrt(sourcenorm);
2709 }
2710 bool skipSolve = false;
2711 // Abort if there is nothing to solve.
2712 if (sourcenorm == 0) {
2713 skipSolve = true;
2714 }
2715
2716 #pragma omp for
2717 for (uint n = 0; n < nodes.size(); n++) {
2718 Node& N = nodes[n];
2720 }
2721
2722 while (!skipSolve && thread_iteration < Ionosphere::solverMaxIterations) {
2723 thread_iteration++;
2724 counter++;
2725
2726 #pragma omp for
2727 for (uint n = 0; n < nodes.size(); n++) {
2728 Node& N = nodes[n];
2730 }
2731
2732 // Calculate bk and gradient vector p
2733 #pragma omp single
2734 {
2735 bknum = 0;
2736#ifdef IONOSPHERE_SORTED_SUMS
2737 set_pos.clear();
2738 set_neg.clear();
2739#endif
2740 }
2741#ifdef IONOSPHERE_SORTED_SUMS
2742 thread_set_pos.clear();
2743 thread_set_neg.clear();
2744 #pragma omp for
2745#else
2746 #pragma omp for reduction(+ : bknum)
2747#endif
2748 for (uint n = 0; n < nodes.size(); n++) {
2749 Node& N = nodes[n];
2751#ifdef IONOSPHERE_SORTED_SUMS
2752 if (incr < 0) {
2753 thread_set_neg.insert(incr);
2754 }
2755 if (incr > 0) {
2756 thread_set_pos.insert(incr);
2757 }
2758#else
2759 bknum += incr;
2760#endif
2761 }
2762
2763#ifdef IONOSPHERE_SORTED_SUMS
2764 #pragma omp critical
2765 {
2766 set_neg.insert(thread_set_neg.begin(), thread_set_neg.end());
2767 set_pos.insert(thread_set_pos.begin(), thread_set_pos.end());
2768 }
2769 #pragma omp barrier
2770 #pragma omp single
2771 {
2772 iSolverReal bknum_pos = 0;
2773 iSolverReal bknum_neg = 0;
2774 for (auto it = set_neg.cbegin(); it != set_neg.cend(); it++) {
2775 bknum_neg += *it;
2776 }
2777 for (auto it = set_pos.crbegin(); it != set_pos.crend(); it++) {
2778 bknum_pos += *it;
2779 }
2780 bknum = bknum_neg + bknum_pos;
2781 }
2782#endif
2783
2784 if (counter == 1) {
2785 // Just use the gradient vector as-is, starting from the best known solution
2786 #pragma omp for
2787 for (uint n = 0; n < nodes.size(); n++) {
2788 Node& N = nodes[n];
2791 }
2792 } else {
2793 // Perform gram-smith orthogonalization to get conjugate gradient
2794 iSolverReal bk = bknum / bkden;
2795 #pragma omp for
2796 for (uint n = 0; n < nodes.size(); n++) {
2797 Node& N = nodes[n];
2802 }
2803 }
2804 bkden = bknum;
2805 if (bkden == 0) {
2806 bkden = 1;
2807 }
2808
2809 // Calculate ak, new solution and new residual
2810 #pragma omp single
2811 {
2812 akden = 0;
2813#ifdef IONOSPHERE_SORTED_SUMS
2814 set_neg.clear();
2815 set_pos.clear();
2816#endif
2817 }
2818#ifdef IONOSPHERE_SORTED_SUMS
2819 thread_set_neg.clear();
2820 thread_set_pos.clear();
2821 #pragma omp for
2822#else
2823 #pragma omp for reduction(+ : akden)
2824#endif
2825 for (uint n = 0; n < nodes.size(); n++) {
2826 Node& N = nodes[n];
2830#ifdef IONOSPHERE_SORTED_SUMS
2831 if (incr < 0) {
2832 thread_set_neg.insert(incr);
2833 }
2834 if (incr > 0) {
2835 thread_set_pos.insert(incr);
2836 }
2837#else
2838 akden += incr;
2839#endif
2841 }
2842#ifdef IONOSPHERE_SORTED_SUMS
2843 #pragma omp critical
2844 {
2845 set_neg.insert(thread_set_neg.begin(), thread_set_neg.end());
2846 set_pos.insert(thread_set_pos.begin(), thread_set_pos.end());
2847 }
2848 #pragma omp barrier
2849 #pragma omp single
2850 {
2851 iSolverReal akden_pos = 0;
2852 iSolverReal akden_neg = 0;
2853 for (auto it = set_neg.cbegin(); it != set_neg.cend(); it++) {
2854 akden_neg += *it;
2855 }
2856 for (auto it = set_pos.crbegin(); it != set_pos.crend(); it++) {
2857 akden_pos += *it;
2858 }
2859 akden = akden_neg + akden_pos;
2860 }
2861#endif
2862 iSolverReal ak = bknum / akden;
2863
2864 #pragma omp for
2865 for (uint n = 0; n < nodes.size(); n++) {
2866 Node& N = nodes[n];
2868 if (ionosphereGrid.gaugeFixing == Pole && n == 0) {
2870 } else if (ionosphereGrid.gaugeFixing == Equator && fabs(N.x[2]) < Ionosphere::innerRadius * sin(Ionosphere::shieldingLatitude * M_PI / 180.0)) {
2872 }
2873 }
2874
2875 // Rebalance the potential by calculating its area integral
2876 if (ionosphereGrid.gaugeFixing == Integral) {
2877 #pragma omp single
2878 { potentialInt = 0; }
2879 #pragma omp for reduction(+ : potentialInt)
2880 for (uint e = 0; e < elements.size(); e++) {
2881 Real area = elementArea(e);
2882 Real effPotential = 0;
2883 for (int c = 0; c < 3; c++) {
2884 effPotential += nodes[elements[e].corners[c]].parameters[ionosphereParameters::SOLUTION];
2885 }
2886
2887 potentialInt += effPotential * area;
2888 }
2889 // Calculate average potential on the sphere
2890 #pragma omp single
2891 { potentialInt /= 4. * M_PI * Ionosphere::innerRadius * Ionosphere::innerRadius; }
2892
2893 // Offset potentials to make it zero
2894 #pragma omp for
2895 for (uint n = 0; n < nodes.size(); n++) {
2896 Node& N = nodes[n];
2897 N.parameters[ionosphereParameters::SOLUTION] -= potentialInt;
2898 }
2899 }
2900
2901 #pragma omp single
2902 {
2903 residualnorm = 0;
2904#ifdef IONOSPHERE_SORTED_SUMS
2905 set_pos.clear();
2906#endif
2907 }
2908#ifdef IONOSPHERE_SORTED_SUMS
2909 thread_set_pos.clear();
2910 #pragma omp for
2911#else
2912 #pragma omp for reduction(+ : residualnorm)
2913#endif
2914 for (uint n = 0; n < nodes.size(); n++) {
2915 Node& N = nodes[n];
2916 // Calculate residual of the new solution. The faster way to do this would be
2917 //
2918 // iSolverReal newresid = N.parameters[ionosphereParameters::RESIDUAL] - ak * N.parameters[ionosphereParameters::ZPARAM];
2919 // and
2920 // N.parameters[ionosphereParameters::RRESIDUAL] -= ak * N.parameters[ionosphereParameters::ZZPARAM];
2921 //
2922 // but doing so leads to numerical inaccuracy due to roundoff errors
2923 // when iteration counts are high (because, for example, mesh node count is high and the matrix condition is bad).
2924 // See https://en.wikipedia.org/wiki/Conjugate_gradient_method#Explicit_residual_calculation
2925 iSolverReal newresid = effectiveSource[n] - Atimes(n, ionosphereParameters::SOLUTION);
2926 if ((ionosphereGrid.gaugeFixing == Pole && n == 0) || (ionosphereGrid.gaugeFixing == Equator && fabs(N.x[2]) < Ionosphere::innerRadius * sin(Ionosphere::shieldingLatitude * M_PI / 180.0))) {
2927 // Don't calculate residual for gauge-pinned nodes
2930 } else {
2933#ifdef IONOSPHERE_SORTED_SUMS
2934 thread_set_pos.insert(newresid * newresid);
2935#else
2936 residualnorm += newresid * newresid;
2937#endif
2938 }
2939 }
2940
2941#ifdef IONOSPHERE_SORTED_SUMS
2942 #pragma omp critical
2943 { set_pos.insert(thread_set_pos.begin(), thread_set_pos.end()); }
2944 #pragma omp barrier
2945 #pragma omp single
2946 {
2947 for (auto it = set_pos.crbegin(); it != set_pos.crend(); it++) {
2948 residualnorm += *it;
2949 }
2950 }
2951#endif
2952
2953 #pragma omp for
2954 for (uint n = 0; n < nodes.size(); n++) {
2955 Node& N = nodes[n];
2957 }
2958
2959 // See if this solved the potential better than before
2960 err = sqrt(residualnorm) / sourcenorm;
2961
2962 if (err < thread_minerr) {
2963 // If yes, this is our new best solution
2964 #pragma omp for
2965 for (uint n = 0; n < nodes.size(); n++) {
2966 Node& N = nodes[n];
2968 }
2969 thread_minerr = err;
2970 failcount = 0;
2971 } else {
2972 // If no, keep going with the best one
2973 #pragma omp for
2974 for (uint n = 0; n < nodes.size(); n++) {
2975 Node& N = nodes[n];
2977 }
2978 failcount++;
2979 }
2980
2982 break;
2983 }
2984 if (failcount > Ionosphere::solverMaxFailureCount || err > Ionosphere::solverMaxErrorGrowthFactor * thread_minerr) {
2985 thread_nRestarts++;
2986 break;
2987 }
2988 } // while
2989
2990 int threadID = 0;
2991#ifdef _OPENMP
2992 threadID = omp_get_thread_num();
2993#endif
2994 if (skipSolve && threadID == 0) {
2995 // sourcenorm was zero, we return zero; return is not allowed inside threaded region
2996 minerr = 0;
2997 minPotentialN = 0;
2998 maxPotentialN = 0;
2999 minPotentialS = 0;
3000 maxPotentialS = 0;
3001 } else {
3002 #pragma omp for reduction(max : maxPotentialN, maxPotentialS) reduction(min : minPotentialN, minPotentialS)
3003 for (uint n = 0; n < nodes.size(); n++) {
3004 Node& N = nodes.at(n);
3006 if (N.x.at(2) > 0) {
3007 minPotentialN = min(minPotentialN, N.parameters.at(ionosphereParameters::SOLUTION));
3008 maxPotentialN = max(maxPotentialN, N.parameters.at(ionosphereParameters::SOLUTION));
3009 } else {
3010 minPotentialS = min(minPotentialS, N.parameters.at(ionosphereParameters::SOLUTION));
3011 maxPotentialS = max(maxPotentialS, N.parameters.at(ionosphereParameters::SOLUTION));
3012 }
3013 }
3014 // Get out the ones we need before exiting the parallel region
3015 if (threadID == 0) {
3016 minerr = thread_minerr;
3017 iteration = thread_iteration;
3018 nRestarts = thread_nRestarts;
3019 }
3020 }
3021
3022 } // #pragma omp parallel
3023 }
3024
3025 // Actual ionosphere object implementation
3026
3028
3030
3032 Readparameters::add("ionosphere.centerX", "X coordinate of ionosphere center (m)", 0.0);
3033 Readparameters::add("ionosphere.centerY", "Y coordinate of ionosphere center (m)", 0.0);
3034 Readparameters::add("ionosphere.centerZ", "Z coordinate of ionosphere center (m)", 0.0);
3035 Readparameters::add("ionosphere.radius", "Radius of the inner simulation boundary (unit is assumed to be R_E if value < 1000, otherwise m).", 1.0e7);
3036 Readparameters::add("ionosphere.innerRadius", "Radius of the ionosphere model (m).", physicalconstants::R_E + 100e3);
3037 Readparameters::add("ionosphere.geometry", "Select the geometry of the ionosphere, 0: inf-norm (diamond), 1: 1-norm (square), 2: 2-norm (circle, DEFAULT), 3: 2-norm cylinder aligned with y-axis, use with polar plane/line dipole.", 2);
3038 Readparameters::add("ionosphere.precedence", "Precedence value of the ionosphere system boundary condition (integer), the higher the stronger.", 2);
3039 Readparameters::add("ionosphere.reapplyUponRestart", "If 0 (default), keep going with the state existing in the restart file. If 1, calls again applyInitialState. Can be used to change boundary condition behaviour during a run.", 0);
3040 Readparameters::add("ionosphere.baseShape", "Select the seed mesh geometry for the spherical ionosphere grid. Options are: fromFile, sphericalFibonacci, tetrahedron, octahedron, icosahedron.", std::string("sphericalFibonacci"));
3041 Readparameters::add("ionosphere.conductivityModel", "Select ionosphere conductivity tensor construction model. Options are: 0=GUMICS style (Vertical B, only SigmaH and SigmaP), 1=Ridley et al 2004 (1000 mho longitudinal conductivity), 2=Koskinen 2011 full conductivity tensor.", 0);
3042 Readparameters::add("ionosphere.ridleyParallelConductivity", "Constant parallel conductivity value. 1000 mho is given without justification by Ridley et al 2004.", 1000);
3043 Readparameters::add("ionosphere.fibonacciNodeNum", "Number of nodes in the spherical fibonacci mesh.", 256);
3044 Readparameters::add("ionosphere.gridFilePath", "Path to the ionosphere grid mesh OBJ or VTK legacy file, if loading grid from file.",std::string(""));
3045 Readparameters::addComposing("ionosphere.refineMinLatitude", "Refine the grid polewards of the given latitude. Multiple of these lines can be given for successive refinement, paired up with refineMaxLatitude lines.");
3046 Readparameters::addComposing("ionosphere.refineMaxLatitude", "Refine the grid equatorwards of the given latitude. Multiple of these lines can be given for successive refinement, paired up with refineMinLatitude lines.");
3047 Readparameters::add("ionosphere.atmosphericModelFile", "Filename to read the MSIS atmosphere data from (default: NRLMSIS.dat)", std::string("NRLMSIS.dat"));
3048 Readparameters::add("ionosphere.recombAlpha", "Ionospheric recombination parameter (m^3/s)", 2.4e-13); // Default value from Schunck & Nagy, Table 8.5
3049 Readparameters::add("ionosphere.ionizationModel", "Ionospheric electron production rate model. Options are: Rees1963, Rees1989, SergienkoIvanov (default), FixedSigma, Robinson2020, Juusola2025.", std::string("SergienkoIvanov"));
3050 Readparameters::add("ionosphere.innerBoundaryVDFmode", "Inner boundary VDF construction method. Options are: FixedMoments, AverageMoments, AverageAllMoments, CopyAndLosscone.", std::string("FixedMoments"));
3051 Readparameters::add("ionosphere.F10_7", "Solar 10.7 cm radio flux (sfu = 10^{-22} W/m^2)", 100);
3052 Readparameters::add("ionosphere.backgroundIonisation", "Background ionoisation due to cosmic rays (mho)", 0.5);
3053 Readparameters::add("ionosphere.fixedSigmaP", "Fixed Pedersen conductivity value for the whole shell, if ionizationModel is 'fixedSigma'", 10.);
3054 Readparameters::add("ionosphere.fixedSigmaH", "Fixed Hall conductivity value for the whole shell, if ionizationModel is 'fixedSigma'", 0.);
3055 Readparameters::add("ionosphere.useEigenSolver", "Whether to use Eigen's BiCGSTAB solver over our home-grown BiCG implementation", false);
3056 Readparameters::add("ionosphere.solverMaxIterations", "Maximum number of iterations for the conjugate gradient solver", 2000);
3057 Readparameters::add("ionosphere.solverRelativeL2ConvergenceThreshold", "Convergence threshold for the relative L2 metric", 1e-6);
3058 Readparameters::add("ionosphere.solverMaxFailureCount", "Maximum number of iterations allowed to diverge before restarting the ionosphere solver", 5);
3059 Readparameters::add("ionosphere.solverMaxErrorGrowthFactor", "Maximum allowed factor of growth with respect to the minimum error before restarting the ionosphere solver", 100);
3060 Readparameters::add("ionosphere.solverGaugeFixing", "Gauge fixing method of the ionosphere solver. Options are: pole, integral, equator", std::string("equator"));
3061 Readparameters::add("ionosphere.shieldingLatitude", "Latitude below which the potential is set to zero in the equator gauge fixing scheme (degree)", 70);
3062 Readparameters::add("ionosphere.solverPreconditioning", "Use preconditioning for the solver? (0/1)", 1);
3063 Readparameters::add("ionosphere.solverUseMinimumResidualVariant", "Use minimum residual variant", 0);
3064 Readparameters::add("ionosphere.solverToggleMinimumResidualVariant", "Toggle use of minimum residual variant at every solver restart", 0);
3065 Readparameters::add("ionosphere.earthAngularVelocity", "Angular velocity of inner boundary convection, in rad/s", 7.2921159e-5);
3066 Readparameters::add("ionosphere.plasmapauseL", "L-shell at which the plasmapause resides (for corotation)", 5.);
3067 Readparameters::add("ionosphere.downmapRadius", "Radius from which FACs are coupled down into the ionosphere. Units are assumed to be RE if value < 1000, otherwise m. If -1: use inner boundary cells.", -1.);
3068 Readparameters::add("ionosphere.downmapSamplingMode", "Method for sampling (smoothing) the downmapped quantities (FACs) to the ionosphere. Options are: Pointwise, Boxcar27.", std::string("Pointwise"));
3069 Readparameters::add("ionosphere.downmapSamplingWidth", "Width for smoothing the Vlasov grid downmapped parameters; see sampling modes for details.", 1.0);
3070
3071 Readparameters::add("ionosphere.unmappedNodeRho", "Electron density of ionosphere nodes that do not connect to the magnetosphere domain.", 1e4);
3072 Readparameters::add("ionosphere.unmappedNodeTe", "Electron temperature of ionosphere nodes that do not connect to the magnetosphere domain.", 1e6);
3073 Readparameters::add("ionosphere.couplingTimescale", "Magnetosphere->Ionosphere coupling timescale (seconds, 0=immediate coupling", 1.);
3074 Readparameters::add("ionosphere.couplingInterval", "Time interval at which the ionosphere is solved (seconds)", 0);
3075
3076 // Per-population parameters
3077 for (uint i = 0; i < getObjectWrapper().particleSpecies.size(); i++) {
3078 const std::string& pop = getObjectWrapper().particleSpecies[i].name;
3079 Readparameters::add(pop + "_ionosphere.rho", "Number density of the ionosphere (m^-3)", 0.0);
3080 Readparameters::add(pop + "_ionosphere.T", "Temperature of the ionosphere (K)", 0.0);
3081 Readparameters::add(pop + "_ionosphere.VX0", "Bulk velocity of ionospheric distribution function in X direction (m/s)", 0.0);
3082 Readparameters::add(pop + "_ionosphere.VY0", "Bulk velocity of ionospheric distribution function in Y direction (m/s)", 0.0);
3083 Readparameters::add(pop + "_ionosphere.VZ0", "Bulk velocity of ionospheric distribution function in Z direction (m/s)", 0.0);
3084 }
3085 }
3086
3088
3089 Readparameters::get("ionosphere.centerX", this->center[0]);
3090 Readparameters::get("ionosphere.centerY", this->center[1]);
3091 Readparameters::get("ionosphere.centerZ", this->center[2]);
3092 Readparameters::get("ionosphere.radius", this->radius);
3093 if (radius < 1000.) {
3094 // If radii are < 1000, assume they are given in R_E.
3096 }
3097
3098 Readparameters::get("ionosphere.geometry", this->geometry);
3099 Readparameters::get("ionosphere.precedence", this->precedence);
3100
3101 uint reapply;
3102 Readparameters::get("ionosphere.reapplyUponRestart", reapply);
3103 this->applyUponRestart = (reapply == 1);
3104
3105 Readparameters::get("ionosphere.baseShape", baseShape);
3106
3107 int cm;
3108 Readparameters::get("ionosphere.conductivityModel", cm);
3110
3111 std::string VDFmodeString;
3112 Readparameters::get("ionosphere.innerBoundaryVDFmode", VDFmodeString);
3113 if (VDFmodeString == "FixedMoments") {
3115 } else if (VDFmodeString == "AverageMoments") {
3117 } else if (VDFmodeString == "AverageAllMoments") {
3119 } else if (VDFmodeString == "CopyAndLosscone") {
3121 } else {
3122 cerr << "(IONOSPHERE) Unknown inner boundary VDF mode \"" << VDFmodeString << "\". Aborting." << endl;
3123 abort();
3124 }
3125
3126 std::string downmapFACsamplingModeString;
3127 Readparameters::get("ionosphere.downmapSamplingMode", downmapFACsamplingModeString);
3128 if(downmapFACsamplingModeString == "Pointwise") {
3130 } else if(downmapFACsamplingModeString == "Boxcar27") {
3132 } else {
3133 cerr << "(IONOSPHERE) Unknown inner boundary downsampling mode \"" << downmapFACsamplingModeString << "\". Aborting." << endl;
3134 abort();
3135 }
3136 Readparameters::get("ionosphere.downmapSamplingWidth", downmapSamplingWidth);
3137
3138
3139 Readparameters::get("ionosphere.ridleyParallelConductivity", ridleyParallelConductivity);
3140 Readparameters::get("ionosphere.fibonacciNodeNum", fibonacciNodeNum);
3141 Readparameters::get("ionosphere.gridFilePath", path);
3142 Readparameters::get("ionosphere.useEigenSolver", useEigenSolver);
3143 Readparameters::get("ionosphere.solverMaxIterations", solverMaxIterations);
3144 Readparameters::get("ionosphere.solverRelativeL2ConvergenceThreshold", solverRelativeL2ConvergenceThreshold);
3145 Readparameters::get("ionosphere.solverMaxFailureCount", solverMaxFailureCount);
3146 Readparameters::get("ionosphere.solverMaxErrorGrowthFactor", solverMaxErrorGrowthFactor);
3147 std::string gaugeFixingString;
3148 Readparameters::get("ionosphere.solverGaugeFixing", gaugeFixingString);
3149 if (gaugeFixingString == "pole") {
3151 } else if (gaugeFixingString == "integral") {
3153 } else if (gaugeFixingString == "equator") {
3155 } else if (gaugeFixingString == "None") {
3157 } else {
3158 cerr << "(IONOSPHERE) Unknown solver gauge fixing method \"" << gaugeFixingString << "\". Aborting." << endl;
3159 abort();
3160 }
3161 Readparameters::get("ionosphere.shieldingLatitude", shieldingLatitude);
3162 Readparameters::get("ionosphere.solverPreconditioning", solverPreconditioning);
3163 Readparameters::get("ionosphere.solverUseMinimumResidualVariant", solverUseMinimumResidualVariant);
3164 Readparameters::get("ionosphere.solverToggleMinimumResidualVariant", solverToggleMinimumResidualVariant);
3165 Readparameters::get("ionosphere.earthAngularVelocity", earthAngularVelocity);
3166 Readparameters::get("ionosphere.plasmapauseL", plasmapauseL);
3167 Readparameters::get("ionosphere.couplingTimescale", couplingTimescale);
3168 Readparameters::get("ionosphere.couplingInterval", couplingInterval);
3169 Readparameters::get("ionosphere.downmapRadius", downmapRadius);
3170 if (downmapRadius < 1000.) {
3172 }
3173 if (downmapRadius < radius) {
3175 }
3176 Readparameters::get("ionosphere.unmappedNodeRho", unmappedNodeRho);
3177 Readparameters::get("ionosphere.unmappedNodeTe", unmappedNodeTe);
3178 Readparameters::get("ionosphere.innerRadius", innerRadius);
3179 FieldTracing::fieldTracingParameters.innerBoundaryRadius = this->innerRadius;
3180 Readparameters::get("ionosphere.refineMinLatitude", refineMinLatitudes);
3181 Readparameters::get("ionosphere.refineMaxLatitude", refineMaxLatitudes);
3182 Readparameters::get("ionosphere.atmosphericModelFile", atmosphericModelFile);
3183 Readparameters::get("ionosphere.recombAlpha", recombAlpha);
3184 std::string ionizationModelString;
3185 Readparameters::get("ionosphere.ionizationModel", ionizationModelString);
3186 if (ionizationModelString == "Rees1963") {
3188 } else if (ionizationModelString == "Rees1989") {
3190 } else if (ionizationModelString == "SergienkoIvanov") {
3192 } else if (ionizationModelString == "Robinson2020") {
3194 } else if (ionizationModelString == "Juusola2025") {
3196 } else if (ionizationModelString == "FixedSigma") {
3198 } else {
3199 cerr << "(IONOSPHERE) Unknown ionization production model \"" << ionizationModelString << "\". Aborting." << endl;
3200 abort();
3201 }
3202 Readparameters::get("ionosphere.F10_7", F10_7);
3203 Readparameters::get("ionosphere.backgroundIonisation", backgroundIonisation);
3204 Readparameters::get("ionosphere.fixedSigmaP", fixedSigmaP);
3205 Readparameters::get("ionosphere.fixedSigmaH", fixedSigmaH);
3206
3207 for (uint i = 0; i < getObjectWrapper().particleSpecies.size(); i++) {
3208 const std::string& pop = getObjectWrapper().particleSpecies[i].name;
3210
3211 Readparameters::get(pop + "_ionosphere.rho", sP.rho);
3212 Readparameters::get(pop + "_ionosphere.VX0", sP.V0[0]);
3213 Readparameters::get(pop + "_ionosphere.VY0", sP.V0[1]);
3214 Readparameters::get(pop + "_ionosphere.VZ0", sP.V0[2]);
3215 Readparameters::get(pop + "_ionosphere.T", sP.T);
3216
3217 // Failsafe, if density or temperature is zero, read from Magnetosphere
3218 // (compare the corresponding verbose handling in projects/Magnetosphere/Magnetosphere.cpp)
3219 if (sP.T == 0) {
3220 Readparameters::get(pop + "_Magnetosphere.T", sP.T);
3221 }
3222 if (sP.rho == 0) {
3223 Readparameters::get(pop + "_Magnetosphere.rho", sP.rho);
3224 }
3225
3226 speciesParams.push_back(sP);
3227 }
3228 }
3229
3231 getParameters();
3232 dynamic = false;
3233
3234 // Sanity check: the ionosphere only makes sense in 3D simulations
3235 if (P::xcells_ini == 1 || P::ycells_ini == 1 || P::zcells_ini == 1) {
3236 cerr << "*************************************************" << endl;
3237 cerr << "* BIG FAT IONOSPHERE ERROR: *" << endl;
3238 cerr << "* *" << endl;
3239 cerr << "* You are trying to run a 2D simulation with an *" << endl;
3240 cerr << "* ionosphere inner boundary. This won't work. *" << endl;
3241 cerr << "* *" << endl;
3242 cerr << "* Most likely, your config file needs to be up- *" << endl;
3243 cerr << "* dated, changing all mentions of \"ionosphere\" *" << endl;
3244 cerr << "* to \"copysphere\". *" << endl;
3245 cerr << "* *" << endl;
3246 cerr << "* This simulation will now crash in the friend- *" << endl;
3247 cerr << "* liest way possible. *" << endl;
3248 cerr << "*************************************************" << endl;
3249 abort();
3250 }
3251
3252 // Initialize ionosphere mesh base shape
3253 if (baseShape == "icosahedron") {
3254 ionosphereGrid.initializeIcosahedron();
3255 } else if (baseShape == "octahedron") {
3256 ionosphereGrid.initializeOctahedron();
3257 } else if (baseShape == "tetrahedron") {
3258 ionosphereGrid.initializeTetrahedron();
3259 } else if (baseShape == "sphericalFibonacci") {
3260 ionosphereGrid.initializeSphericalFibonacci(fibonacciNodeNum);
3261 } else if (baseShape == "fromFile") {
3262 ionosphereGrid.initializeGridFromFile(path);
3263 } else {
3264 cerr << "(IONOSPHERE) Unknown mesh base shape \"" << baseShape << "\". Aborting." << endl;
3265 abort();
3266 }
3267
3268 // Refine the base shape to acheive desired resolution
3269 auto refineBetweenLatitudes = [](Real phi1, Real phi2) -> void {
3270 uint numElems = ionosphereGrid.elements.size();
3271
3272 for (uint i = 0; i < numElems; i++) {
3273 Real mean_z = 0;
3274 mean_z = ionosphereGrid.nodes[ionosphereGrid.elements[i].corners[0]].x[2];
3275 mean_z += ionosphereGrid.nodes[ionosphereGrid.elements[i].corners[1]].x[2];
3276 mean_z += ionosphereGrid.nodes[ionosphereGrid.elements[i].corners[2]].x[2];
3277 mean_z /= 3.;
3278
3279 if (fabs(mean_z) >= sin(phi1 * M_PI / 180.) * Ionosphere::innerRadius && fabs(mean_z) <= sin(phi2 * M_PI / 180.) * Ionosphere::innerRadius) {
3280 ionosphereGrid.subdivideElement(i);
3281 }
3282 }
3283 };
3284
3285 // Refine the mesh between the given latitudes
3286 for (uint i = 0; i < max(refineMinLatitudes.size(), refineMaxLatitudes.size()); i++) {
3287 Real lmin;
3288 if (i < refineMinLatitudes.size()) {
3289 lmin = refineMinLatitudes[i];
3290 } else {
3291 lmin = 0.;
3292 }
3293 Real lmax;
3294 if (i < refineMaxLatitudes.size()) {
3295 lmax = refineMaxLatitudes[i];
3296 } else {
3297 lmax = 90.;
3298 }
3299 refineBetweenLatitudes(lmin, lmax);
3300 }
3301 ionosphereGrid.stitchRefinementInterfaces();
3302
3303 // Set up ionospheric atmosphere model
3304 ionosphereGrid.readAtmosphericModelFile(atmosphericModelFile.c_str());
3305
3306 // iniSysBoundary is only called once, generateTemplateCell must
3307 // init all particle species
3308 generateTemplateCell(project);
3309 }
3310
3311 static Real getR(creal x, creal y, creal z, uint geometry, Real center[3]) {
3312
3313 Real r;
3314
3315 switch (geometry) {
3316 case 0:
3317 // infinity-norm, result is a diamond/square with diagonals aligned on the axes in 2D
3318 r = fabs(x - center[0]) + fabs(y - center[1]) + fabs(z - center[2]);
3319 break;
3320 case 1:
3321 // 1-norm, result is is a grid-aligned square in 2D
3322 r = max(max(fabs(x - center[0]), fabs(y - center[1])), fabs(z - center[2]));
3323 break;
3324 case 2:
3325 // 2-norm (Cartesian), result is a circle in 2D
3326 r = sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1]) + (z - center[2]) * (z - center[2]));
3327 break;
3328 case 3:
3329 // 2-norm (Cartesian) cylinder aligned on y-axis
3330 r = sqrt((x - center[0]) * (x - center[0]) + (z - center[2]) * (z - center[2]));
3331 break;
3332 default:
3333 std::cerr << __FILE__ << ":" << __LINE__ << ":" << "ionosphere.geometry has to be 0, 1 or 2." << std::endl;
3334 abort();
3335 }
3336
3337 return r;
3338 }
3339
3340 void Ionosphere::assignSysBoundary(dccrg::Dccrg<SpatialCell, dccrg::Cartesian_Geometry>& mpiGrid, fsgrids::technicalspan technical, FieldSolverGrid& fsgrid) {
3341 const vector<CellID>& cells = getLocalCells();
3342 for (uint i = 0; i < cells.size(); i++) {
3343 if (mpiGrid[cells[i]]->sysBoundaryFlag == sysboundarytype::DO_NOT_COMPUTE) {
3344 continue;
3345 }
3346
3347 creal* const cellParams = &(mpiGrid[cells[i]]->parameters[0]);
3348 creal dx = cellParams[CellParams::DX];
3349 creal dy = cellParams[CellParams::DY];
3350 creal dz = cellParams[CellParams::DZ];
3351 creal x = cellParams[CellParams::XCRD] + 0.5 * dx;
3352 creal y = cellParams[CellParams::YCRD] + 0.5 * dy;
3353 creal z = cellParams[CellParams::ZCRD] + 0.5 * dz;
3354
3355 if (getR(x, y, z, this->geometry, this->center) < this->radius) {
3356 mpiGrid[cells[i]]->sysBoundaryFlag = this->getIndex();
3357 }
3358 }
3359 }
3360
3361 void Ionosphere::applyInitialState(dccrg::Dccrg<SpatialCell, dccrg::Cartesian_Geometry>& mpiGrid, fsgrids::technicalspan technical, FieldSolverGrid& fsgrid, fsgrids::perbspan perb, fsgrids::bgbspan bgb, Project& project) {
3362 const vector<CellID>& cells = getLocalCells();
3363 // #pragma omp parallel for
3364 for (uint i = 0; i < cells.size(); ++i) {
3365 SpatialCell* cell = mpiGrid[cells[i]];
3366 if (cell->sysBoundaryFlag != this->getIndex())
3367 continue;
3368
3369 for (uint popID = 0; popID < getObjectWrapper().particleSpecies.size(); ++popID) {
3370 setCellFromTemplate(cell, popID);
3371#ifdef DEBUG_VLASIATOR
3372 // Verify current mesh and blocks
3373 if (!cell->checkMesh(popID)) {
3374 printf("ERROR in vmesh check: %s at %d\n", __FILE__, __LINE__);
3375 }
3376#endif
3377 }
3378 }
3379 }
3380
3382 phiprof::Timer timer{"Ionosphere::fieldSolverGetNormalDirection"};
3383 std::array<Real, 3> normalDirection{{0.0, 0.0, 0.0}};
3384
3385 static creal DIAG2 = 1.0 / sqrt(2.0);
3386 static creal DIAG3 = 1.0 / sqrt(3.0);
3387 const auto& gridSpacing = fsgrid.getGridSpacing();
3388
3389 creal dx = gridSpacing[0];
3390 creal dy = gridSpacing[1];
3391 creal dz = gridSpacing[2];
3392 const std::array<fsgrid::FsSize_t, 3> globalIndices = fsgrid.localToGlobal(i, j, k);
3393 creal x = P::xmin + (convert<Real>(globalIndices[0]) + 0.5) * dx;
3394 creal y = P::ymin + (convert<Real>(globalIndices[1]) + 0.5) * dy;
3395 creal z = P::zmin + (convert<Real>(globalIndices[2]) + 0.5) * dz;
3396 creal xsign = divideIfNonZero(x, fabs(x));
3397 creal ysign = divideIfNonZero(y, fabs(y));
3398 creal zsign = divideIfNonZero(z, fabs(z));
3399
3400 Real length = 0.0;
3401
3402 if (Parameters::xcells_ini == 1) {
3403 if (Parameters::ycells_ini == 1) {
3404 if (Parameters::zcells_ini == 1) {
3405 // X,Y,Z
3406 std::cerr << __FILE__ << ":" << __LINE__ << ":" << "What do you expect to do with a single-cell simulation of ionosphere boundary type? Stop kidding." << std::endl;
3407 abort();
3408 // end of X,Y,Z
3409 } else {
3410 // X,Y
3411 normalDirection[2] = zsign;
3412 // end of X,Y
3413 }
3414 } else if (Parameters::zcells_ini == 1) {
3415 // X,Z
3416 normalDirection[1] = ysign;
3417 // end of X,Z
3418 } else {
3419 // X
3420 switch (this->geometry) {
3421 case 0:
3422 normalDirection[1] = DIAG2 * ysign;
3423 normalDirection[2] = DIAG2 * zsign;
3424 break;
3425 case 1:
3426 if (fabs(y) == fabs(z)) {
3427 normalDirection[1] = ysign * DIAG2;
3428 normalDirection[2] = zsign * DIAG2;
3429 break;
3430 }
3431 if (fabs(y) > (this->radius - dy)) {
3432 normalDirection[1] = ysign;
3433 break;
3434 }
3435 if (fabs(z) > (this->radius - dz)) {
3436 normalDirection[2] = zsign;
3437 break;
3438 }
3439 if (fabs(y) > (this->radius - 2.0 * dy)) {
3440 normalDirection[1] = ysign;
3441 break;
3442 }
3443 if (fabs(z) > (this->radius - 2.0 * dz)) {
3444 normalDirection[2] = zsign;
3445 break;
3446 }
3447 break;
3448 case 2:
3449 length = sqrt(y * y + z * z);
3450 normalDirection[1] = y / length;
3451 normalDirection[2] = z / length;
3452 break;
3453 default:
3454 std::cerr << __FILE__ << ":" << __LINE__ << ":" << "ionosphere.geometry has to be 0, 1 or 2 with this grid shape." << std::endl;
3455 abort();
3456 }
3457 // end of X
3458 }
3459 } else if (Parameters::ycells_ini == 1) {
3460 if (Parameters::zcells_ini == 1) {
3461 // Y,Z
3462 normalDirection[0] = xsign;
3463 // end of Y,Z
3464 } else {
3465 // Y
3466 switch (this->geometry) {
3467 case 0:
3468 normalDirection[0] = DIAG2 * xsign;
3469 normalDirection[2] = DIAG2 * zsign;
3470 break;
3471 case 1:
3472 if (fabs(x) == fabs(z)) {
3473 normalDirection[0] = xsign * DIAG2;
3474 normalDirection[2] = zsign * DIAG2;
3475 break;
3476 }
3477 if (fabs(x) > (this->radius - dx)) {
3478 normalDirection[0] = xsign;
3479 break;
3480 }
3481 if (fabs(z) > (this->radius - dz)) {
3482 normalDirection[2] = zsign;
3483 break;
3484 }
3485 if (fabs(x) > (this->radius - 2.0 * dx)) {
3486 normalDirection[0] = xsign;
3487 break;
3488 }
3489 if (fabs(z) > (this->radius - 2.0 * dz)) {
3490 normalDirection[2] = zsign;
3491 break;
3492 }
3493 break;
3494 case 2:
3495 case 3:
3496 length = sqrt(x * x + z * z);
3497 normalDirection[0] = x / length;
3498 normalDirection[2] = z / length;
3499 break;
3500 default:
3501 std::cerr << __FILE__ << ":" << __LINE__ << ":" << "ionosphere.geometry has to be 0, 1, 2 or 3 with this grid shape." << std::endl;
3502 abort();
3503 }
3504 // end of Y
3505 }
3506 } else if (Parameters::zcells_ini == 1) {
3507 // Z
3508 switch (this->geometry) {
3509 case 0:
3510 normalDirection[0] = DIAG2 * xsign;
3511 normalDirection[1] = DIAG2 * ysign;
3512 break;
3513 case 1:
3514 if (fabs(x) == fabs(y)) {
3515 normalDirection[0] = xsign * DIAG2;
3516 normalDirection[1] = ysign * DIAG2;
3517 break;
3518 }
3519 if (fabs(x) > (this->radius - dx)) {
3520 normalDirection[0] = xsign;
3521 break;
3522 }
3523 if (fabs(y) > (this->radius - dy)) {
3524 normalDirection[1] = ysign;
3525 break;
3526 }
3527 if (fabs(x) > (this->radius - 2.0 * dx)) {
3528 normalDirection[0] = xsign;
3529 break;
3530 }
3531 if (fabs(y) > (this->radius - 2.0 * dy)) {
3532 normalDirection[1] = ysign;
3533 break;
3534 }
3535 break;
3536 case 2:
3537 length = sqrt(x * x + y * y);
3538 normalDirection[0] = x / length;
3539 normalDirection[1] = y / length;
3540 break;
3541 default:
3542 std::cerr << __FILE__ << ":" << __LINE__ << ":" << "ionosphere.geometry has to be 0, 1 or 2 with this grid shape." << std::endl;
3543 abort();
3544 }
3545 // end of Z
3546 } else {
3547 // 3D
3548 switch (this->geometry) {
3549 case 0:
3550 normalDirection[0] = DIAG3 * xsign;
3551 normalDirection[1] = DIAG3 * ysign;
3552 normalDirection[2] = DIAG3 * zsign;
3553 break;
3554 case 1:
3555 if (fabs(x) == fabs(y) && fabs(x) == fabs(z) && fabs(x) > this->radius - dx) {
3556 normalDirection[0] = xsign * DIAG3;
3557 normalDirection[1] = ysign * DIAG3;
3558 normalDirection[2] = zsign * DIAG3;
3559 break;
3560 }
3561 if (fabs(x) == fabs(y) && fabs(x) == fabs(z) && fabs(x) > this->radius - 2.0 * dx) {
3562 normalDirection[0] = xsign * DIAG3;
3563 normalDirection[1] = ysign * DIAG3;
3564 normalDirection[2] = zsign * DIAG3;
3565 break;
3566 }
3567 if (fabs(x) == fabs(y) && fabs(x) > this->radius - dx && fabs(z) < this->radius - dz) {
3568 normalDirection[0] = xsign * DIAG2;
3569 normalDirection[1] = ysign * DIAG2;
3570 normalDirection[2] = 0.0;
3571 break;
3572 }
3573 if (fabs(y) == fabs(z) && fabs(y) > this->radius - dy && fabs(x) < this->radius - dx) {
3574 normalDirection[0] = 0.0;
3575 normalDirection[1] = ysign * DIAG2;
3576 normalDirection[2] = zsign * DIAG2;
3577 break;
3578 }
3579 if (fabs(x) == fabs(z) && fabs(x) > this->radius - dx && fabs(y) < this->radius - dy) {
3580 normalDirection[0] = xsign * DIAG2;
3581 normalDirection[1] = 0.0;
3582 normalDirection[2] = zsign * DIAG2;
3583 break;
3584 }
3585 if (fabs(x) == fabs(y) && fabs(x) > this->radius - 2.0 * dx && fabs(z) < this->radius - 2.0 * dz) {
3586 normalDirection[0] = xsign * DIAG2;
3587 normalDirection[1] = ysign * DIAG2;
3588 normalDirection[2] = 0.0;
3589 break;
3590 }
3591 if (fabs(y) == fabs(z) && fabs(y) > this->radius - 2.0 * dy && fabs(x) < this->radius - 2.0 * dx) {
3592 normalDirection[0] = 0.0;
3593 normalDirection[1] = ysign * DIAG2;
3594 normalDirection[2] = zsign * DIAG2;
3595 break;
3596 }
3597 if (fabs(x) == fabs(z) && fabs(x) > this->radius - 2.0 * dx && fabs(y) < this->radius - 2.0 * dy) {
3598 normalDirection[0] = xsign * DIAG2;
3599 normalDirection[1] = 0.0;
3600 normalDirection[2] = zsign * DIAG2;
3601 break;
3602 }
3603 if (fabs(x) > (this->radius - dx)) {
3604 normalDirection[0] = xsign;
3605 break;
3606 }
3607 if (fabs(y) > (this->radius - dy)) {
3608 normalDirection[1] = ysign;
3609 break;
3610 }
3611 if (fabs(z) > (this->radius - dz)) {
3612 normalDirection[2] = zsign;
3613 break;
3614 }
3615 if (fabs(x) > (this->radius - 2.0 * dx)) {
3616 normalDirection[0] = xsign;
3617 break;
3618 }
3619 if (fabs(y) > (this->radius - 2.0 * dy)) {
3620 normalDirection[1] = ysign;
3621 break;
3622 }
3623 if (fabs(z) > (this->radius - 2.0 * dz)) {
3624 normalDirection[2] = zsign;
3625 break;
3626 }
3627 break;
3628 case 2:
3629 length = sqrt(x * x + y * y + z * z);
3630 normalDirection[0] = x / length;
3631 normalDirection[1] = y / length;
3632 normalDirection[2] = z / length;
3633 break;
3634 case 3:
3635 length = sqrt(x * x + z * z);
3636 normalDirection[0] = x / length;
3637 normalDirection[2] = z / length;
3638 break;
3639 default:
3640 std::cerr << __FILE__ << ":" << __LINE__ << ":" << "ionosphere.geometry has to be 0, 1, 2 or 3 with this grid shape." << std::endl;
3641 abort();
3642 }
3643 // end of 3D
3644 }
3645
3646 return normalDirection;
3647 }
3648
3661 const std::array<Real, 3>& gridSpacing,
3662 const std::array<fsgrid::FsSize_t, 3>& globalCoordinates,
3663 const fsgrid::FsStencil& stencil,
3664 cuint component
3665 ) {
3666 const uint32_t perbComponent = fsgrids::bfield::PERBX + component;
3667 const uint32_t bitfield = 1 << component;
3668
3669 // clang-format off
3670 static constexpr std::array permutations = {
3671 std::array {
3672 0, 1, 2, 3, 4, 5,
3673 },
3674 std::array {
3675 2, 3, 0, 1, 4, 5,
3676 },
3677 std::array {
3678 4, 5, 0, 1, 2, 3,
3679 },
3680 };
3681
3682 const std::array permutation = permutations[component];
3683
3684 const std::array<size_t, 6> inds = {
3685 stencil.moo(),
3686 stencil.poo(),
3687 stencil.omo(),
3688 stencil.opo(),
3689 stencil.oom(),
3690 stencil.oop(),
3691 };
3692 // clang-format on
3693
3694 auto bitFieldSet = [&bitfield](auto& tech) { return (tech.SOLVE & bitfield) == bitfield; };
3695 auto sbLayerIsOne = [](auto& tech) { return tech.sysBoundaryLayer == 1; };
3696 auto averageNeigbours = [&technical, &b, &inds, &permutation, &perbComponent, &bitFieldSet](auto begin, auto end, auto& sum, auto& nCells) {
3697 for (size_t i = begin; i < end; i++) {
3698 const auto j = inds[permutation[i]];
3699 if (bitFieldSet(technical[j])) {
3700 sum += b[j][perbComponent];
3701 nCells++;
3702 }
3703 }
3704 };
3705
3706 auto averageAllNeighbours = [&stencil, &technical, &b, &perbComponent](auto predicateLambda, auto& sum, auto& nCells) {
3707 for (const auto& i : stencil.indices()) {
3708 if (predicateLambda(technical[i])) {
3709 sum += b[i][perbComponent];
3710 nCells++;
3711 }
3712 }
3713 };
3714
3715 Real sum = 0.0;
3716 uint nCells = 0;
3717 if (sbLayerIsOne(technical[stencil.ooo()])) {
3718 averageNeigbours(0ul, 2ul, sum, nCells);
3719
3720 if (nCells == 0) {
3721 averageNeigbours(2ul, 6ul, sum, nCells);
3722 }
3723
3724 if (nCells == 0) {
3725 averageAllNeighbours(bitFieldSet, sum, nCells);
3726 }
3727 } else {
3728 // L2 cells
3729 averageAllNeighbours(sbLayerIsOne, sum, nCells);
3730 }
3731
3732 if (nCells == 0) {
3733 cerr << __FILE__ << ":" << __LINE__ << ": ERROR: this should not have fallen through." << endl;
3734 sum = 0.0;
3735 nCells = 1;
3736 }
3737
3738 return sum / nCells;
3739 }
3740
3741 void Ionosphere::fieldSolverBoundaryCondElectricField(fsgrids::efieldspan e, const fsgrid::FsStencil& stencil, cuint component) {
3742 e[stencil.ooo()][fsgrids::efield::EX + component] = 0.0;
3743 }
3744
3745 void Ionosphere::fieldSolverBoundaryCondHallElectricField(fsgrids::ehallspan ehall, const fsgrid::FsStencil& stencil, cuint component) {
3746 std::array<Real, fsgrids::ehall::N_EHALL>& cp = ehall[stencil.ooo()];
3747 switch (component) {
3748 case 0:
3753 break;
3754 case 1:
3759 break;
3760 case 2:
3765 break;
3766 default:
3767 cerr << __FILE__ << ":" << __LINE__ << ":" << " Invalid component" << endl;
3768 }
3769 }
3770
3771 void Ionosphere::fieldSolverBoundaryCondGradPeElectricField(fsgrids::egradpespan EGradPe, const fsgrid::FsStencil& stencil, cuint component) {
3772 EGradPe[stencil.ooo()][fsgrids::egradpe::EXGRADPE + component] = 0.0;
3773 }
3774
3775 void Ionosphere::fieldSolverBoundaryCondDerivatives(fsgrids::dperbspan dperb, fsgrids::dmomentsspan dmoments, const fsgrid::FsStencil& stencil, cuint RKCase, cuint component) {
3776 this->setCellDerivativesToZero(dperb, dmoments, stencil, component);
3777 }
3778
3779 void Ionosphere::fieldSolverBoundaryCondBVOLDerivatives(fsgrids::volspan vols, const fsgrid::FsStencil& stencil, cuint component) {
3780 // FIXME This should be OK as the BVOL derivatives are only used for Lorentz force JXB, which is not applied on the ionosphere cells.
3781 this->setCellBVOLDerivativesToZero(vols, stencil, component);
3782 }
3783
3784 void Ionosphere::mapCellPotentialAndGetEXBDrift(std::array<Real, CellParams::N_SPATIAL_CELL_PARAMS>& cellParams) {
3785 // Get potential upmapped from six points
3786 // (Cell's face centres)
3787 // inside the cell to calculate E
3788 const Real xmin = cellParams[CellParams::XCRD];
3789 const Real ymin = cellParams[CellParams::YCRD];
3790 const Real zmin = cellParams[CellParams::ZCRD];
3791 const Real xmax = xmin + cellParams[CellParams::DX];
3792 const Real ymax = ymin + cellParams[CellParams::DY];
3793 const Real zmax = zmin + cellParams[CellParams::DZ];
3794 const Real xcen = 0.5 * (xmin + xmax);
3795 const Real ycen = 0.5 * (ymin + ymax);
3796 const Real zcen = 0.5 * (zmin + zmax);
3797 std::array<std::array<Real, 3>, 6> tracepoints;
3798 tracepoints[0] = {xmin, ycen, zcen};
3799 tracepoints[1] = {xmax, ycen, zcen};
3800 tracepoints[2] = {xcen, ymin, zcen};
3801 tracepoints[3] = {xcen, ymax, zcen};
3802 tracepoints[4] = {xcen, ycen, zmin};
3803 tracepoints[5] = {xcen, ycen, zmax};
3804 std::array<Real, 6> potentials;
3805 for (int i = 0; i < 6; i++) {
3806 // Get potential at each of these 6 points
3807 potentials[i] = ionosphereGrid.interpolateUpmappedPotential(tracepoints[i]);
3808 }
3809
3810 // Calculate E from potential differences as E = -grad(phi)
3811 Vec3d E({
3812 (potentials[0] - potentials[1]) / cellParams[CellParams::DX],
3813 (potentials[2] - potentials[3]) / cellParams[CellParams::DY],
3814 (potentials[4] - potentials[5]) / cellParams[CellParams::DZ]});
3815 Vec3d B({
3816 cellParams[CellParams::BGBXVOL] + cellParams[CellParams::PERBXVOL],
3817 cellParams[CellParams::BGBYVOL] + cellParams[CellParams::PERBYVOL],
3818 cellParams[CellParams::BGBZVOL] + cellParams[CellParams::PERBZVOL]});
3819
3820 // Add E from neutral wind convection for all cells with L <= 5
3821 Vec3d Omega(0, 0, Ionosphere::earthAngularVelocity); // Earth rotation vector
3822 Vec3d r(xcen, ycen, zcen);
3823 Vec3d vn = cross_product(Omega, r);
3824
3826 if (radius / physicalconstants::R_E <= Ionosphere::plasmapauseL * (r[0] * r[0] + r[1] * r[1]) / (radius * radius)) {
3827 E -= cross_product(vn, B);
3828 }
3829
3830 const Real Bsqr = B[0] * B[0] + B[1] * B[1] + B[2] * B[2];
3831
3832 // Calculate cell bulk velocity as E x B / B^2
3833 cellParams[CellParams::BULKV_FORCING_X] = (E[1] * B[2] - E[2] * B[1]) / Bsqr;
3834 cellParams[CellParams::BULKV_FORCING_Y] = (E[2] * B[0] - E[0] * B[2]) / Bsqr;
3835 cellParams[CellParams::BULKV_FORCING_Z] = (E[0] * B[1] - E[1] * B[0]) / Bsqr;
3836 }
3837
3838 void Ionosphere::vlasovBoundaryCondition(dccrg::Dccrg<SpatialCell, dccrg::Cartesian_Geometry>& mpiGrid, const CellID& cellID, const uint popID, const bool calculate_V_moments) {
3839 // TODO Make this a more elegant solution
3840 // Now it's hacky as the counter is incremented in vlasiator.cpp
3841 if (globalflags::ionosphereJustSolved) { // else we don't update this boundary
3842
3843 // If we are to couple to the ionosphere grid, we better be part of its communicator.
3844 assert(ionosphereGrid.communicator != MPI_COMM_NULL);
3845
3846 mapCellPotentialAndGetEXBDrift(mpiGrid[cellID]->parameters);
3847 std::array<Real, 3> vDrift = {mpiGrid[cellID]->parameters[CellParams::BULKV_FORCING_X], mpiGrid[cellID]->parameters[CellParams::BULKV_FORCING_Y], mpiGrid[cellID]->parameters[CellParams::BULKV_FORCING_Z]};
3848
3849 // Select representative moments for the VDFs
3850 Real temperature = 0;
3851 Real density = 0;
3852 switch (boundaryVDFmode) {
3853#pragma GCC diagnostic push
3854#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
3855 case FixedMoments:
3856 density = speciesParams[popID].rho;
3857 temperature = speciesParams[popID].T;
3858 break;
3859 case AverageAllMoments: // Fall through (handled by if further down)
3860 case AverageMoments:
3861 // Maxwellian VDF boundary modes
3862 {
3863 Real pressure = 0, vx = 0, vy = 0, vz = 0;
3864 // Average density and temperature from the nearest cells
3865 const vector<CellID>& closestCells = getAllClosestNonsysboundaryCells(cellID);
3866 for (CellID celli : closestCells) {
3867 density += mpiGrid[celli]->parameters[CellParams::RHOM];
3868 pressure += mpiGrid[celli]->parameters[CellParams::P_11] + mpiGrid[celli]->parameters[CellParams::P_22] + mpiGrid[celli]->parameters[CellParams::P_33];
3869 vx += mpiGrid[celli]->parameters[CellParams::VX];
3870 vy += mpiGrid[celli]->parameters[CellParams::VY];
3871 vz += mpiGrid[celli]->parameters[CellParams::VZ];
3872 }
3873 density /= closestCells.size() * physicalconstants::MASS_PROTON;
3874 vx /= closestCells.size();
3875 vy /= closestCells.size();
3876 vz /= closestCells.size();
3877 pressure /= 3.0 * closestCells.size();
3878 // TODO make this multipop
3879 temperature = pressure / (density * physicalconstants::K_B);
3880
3882 vDrift[0] += vx;
3883 vDrift[1] += vy;
3884 vDrift[2] += vz;
3885 }
3886 }
3887 break;
3888 case CopyAndLosscone:
3889 // This is handled below
3890 break;
3891 }
3892#pragma GCC diagnostic pop
3893
3894 // Fill velocity space
3895 switch (boundaryVDFmode) {
3896 case FixedMoments:
3897 case AverageAllMoments:
3898 case AverageMoments: {
3899 // Fill velocity space with new maxwellian data
3900 SpatialCell& cell = *mpiGrid[cellID];
3901 cell.clear(popID, false); // Clear previous velocity space completely, do not de-allocate memory
3902 creal initRho = density;
3903 creal initT = temperature;
3904 creal initV0X = vDrift[0];
3905 creal initV0Y = vDrift[1];
3906 creal initV0Z = vDrift[2];
3907 creal mass = getObjectWrapper().particleSpecies[popID].mass;
3908
3909 // Find list of blocks to initialize.
3910 const uint nRequested = SBC::findMaxwellianBlocksToInitialize(popID, cell, initRho, initT, initV0X, initV0Y, initV0Z);
3911 // stores in vmesh->getGrid() (localToGlobalMap)
3912 // with count in cell.get_population(popID).N_blocks
3913
3914 // Resize and populate mesh
3915 cell.prepare_to_receive_blocks(popID);
3916
3917 // Set the reservation value (capacity is increased in add_velocity_blocks
3918
3919 // const Realf minValue = cell.getVelocityBlockMinValue(popID);
3920
3921 // fills v-space into target
3922
3923#ifdef USE_GPU
3926#else
3929#endif
3930 // Loop over blocks
3931 Realf rhosum = 0;
3933 {WID, WID, WID, nRequested},
3934 ARCH_LOOP_LAMBDA(const uint i, const uint j, const uint k, const uint initIndex, Realf* lsum) {
3935 vmesh::GlobalID* GIDlist = vmesh->getGrid()->data();
3936 Realf* bufferData = VBC->getData();
3937 const vmesh::GlobalID blockGID = GIDlist[initIndex];
3938 // Calculate parameters for new block
3939 Real blockCoords[6];
3940 vmesh->getBlockInfo(blockGID, &blockCoords[0]);
3941 creal vxBlock = blockCoords[0];
3942 creal vyBlock = blockCoords[1];
3943 creal vzBlock = blockCoords[2];
3944 creal dvxCell = blockCoords[3];
3945 creal dvyCell = blockCoords[4];
3946 creal dvzCell = blockCoords[5];
3947 ARCH_INNER_BODY(i, j, k, initIndex, lsum) {
3948 creal vx = vxBlock + (i + 0.5) * dvxCell - initV0X;
3949 creal vy = vyBlock + (j + 0.5) * dvyCell - initV0Y;
3950 creal vz = vzBlock + (k + 0.5) * dvzCell - initV0Z;
3951 const Realf value = projects::MaxwellianPhaseSpaceDensity(vx, vy, vz, initT, initRho, mass);
3952 bufferData[initIndex * WID3 + k * WID2 + j * WID + i] = value;
3953 // lsum[0] += value;
3954 };
3955 },
3956 rhosum);
3957
3958#ifdef USE_GPU
3959 // Set and apply the reservation value
3960 cell.setReservation(popID, nRequested, true); // Force to this value
3961 cell.applyReservation(popID);
3962#endif
3963 } // end case several
3964 break;
3965 case CopyAndLosscone: {
3966 // GPUTODO: Untested after porting to new initialization
3967 Real vNeighboursX = 0;
3968 Real vNeighboursY = 0;
3969 Real vNeighboursZ = 0;
3970 Real pressure = 0;
3971 // Get moments from the nearest cells
3972 const vector<CellID>& closestCells = getAllClosestNonsysboundaryCells(cellID);
3973 for (CellID celli : closestCells) {
3974 density += mpiGrid[celli]->parameters[CellParams::RHOM];
3975 pressure += mpiGrid[celli]->parameters[CellParams::P_11] + mpiGrid[celli]->parameters[CellParams::P_22] + mpiGrid[celli]->parameters[CellParams::P_33];
3976 vNeighboursX += mpiGrid[celli]->parameters[CellParams::VX];
3977 vNeighboursY += mpiGrid[celli]->parameters[CellParams::VY];
3978 vNeighboursZ += mpiGrid[celli]->parameters[CellParams::VZ];
3979 }
3980 density /= closestCells.size() * physicalconstants::MASS_PROTON;
3981 pressure /= 3.0 * closestCells.size();
3982 vNeighboursX /= closestCells.size();
3983 vNeighboursY /= closestCells.size();
3984 vNeighboursZ /= closestCells.size();
3985 creal temperature = pressure / (density * physicalconstants::K_B);
3986 // Fill velocity space with new VDF data. This consists of three parts:
3987 // 1. For the downwards-moving part of the VDF (dot(v,r) < 0), simply fill a maxwellian with the averaged density and pressure.
3988 // 2. For upwards-moving velocity cells outside the loss cone, take the reflected value from point 1.
3989 // 3. Add an ionospheric outflow maxwellian.
3990 SpatialCell& cell = *mpiGrid[cellID];
3994 const Real Bsqr = BX * BX + BY * BY + BZ * BZ;
3998
3999 cell.clear(popID, false); // Clear previous velocity space completely, do not de-allocate memory
4000 creal initRho = speciesParams[popID].rho;
4001 creal initT = speciesParams[popID].T;
4002 creal initV0X = vDrift[0];
4003 creal initV0Y = vDrift[1];
4004 creal initV0Z = vDrift[2];
4005 creal mass = getObjectWrapper().particleSpecies[popID].mass;
4006
4007 // Find list of blocks to initialize.
4008 // WARNING: This now only finds blocks based on the outflow population, not including the copied losscone.
4009 const uint nRequested = SBC::findMaxwellianBlocksToInitialize(popID, cell, initRho, initT, initV0X, initV0Y, initV0Z);
4010 // stores in vmesh->getGrid() (localToGlobalMap)
4011 // with count in cell.get_population(popID).N_blocks
4012
4013 // Resize and populate mesh
4014 cell.prepare_to_receive_blocks(popID);
4015
4016 // Set the reservation value (capacity is increased in add_velocity_blocks
4017 // const Realf minValue = cell.getVelocityBlockMinValue(popID);
4018
4019 // fills v-space into target
4020
4021#ifdef USE_GPU
4024#else
4027#endif
4028 // Loop over blocks
4029 Realf rhosum = 0;
4031 {WID, WID, WID, nRequested},
4032 ARCH_LOOP_LAMBDA(const uint i, const uint j, const uint k, const uint initIndex, Realf* lsum) {
4033 vmesh::GlobalID* GIDlist = vmesh->getGrid()->data();
4034 Realf* bufferData = VBC->getData();
4035 const vmesh::GlobalID blockGID = GIDlist[initIndex];
4036 // Calculate parameters for new block
4037 Real blockCoords[6];
4038 vmesh->getBlockInfo(blockGID, &blockCoords[0]);
4039 creal vxBlock = blockCoords[0];
4040 creal vyBlock = blockCoords[1];
4041 creal vzBlock = blockCoords[2];
4042 creal dvxCell = blockCoords[3];
4043 creal dvyCell = blockCoords[4];
4044 creal dvzCell = blockCoords[5];
4045 ARCH_INNER_BODY(i, j, k, initIndex, lsum) {
4046 creal vx = vxBlock + (i + 0.5) * dvxCell;
4047 creal vy = vyBlock + (j + 0.5) * dvyCell;
4048 creal vz = vzBlock + (k + 0.5) * dvzCell;
4049
4050 // Calculate pitchangle cosine
4051 creal mu = (vx * BX + vy * BY + vz * BZ) / sqrt(Bsqr) / sqrt(vx * vx + vy * vy + vz * vz);
4052 // Radial velocity component
4053 creal rlength = sqrt(RX * RX + RY * RY + RZ * RZ);
4054 creal RnormX = RX / rlength;
4055 creal RnormY = RY / rlength;
4056 creal RnormZ = RZ / rlength;
4057 creal vdotr = (vx * RnormX + vy * RnormY * vz * RnormZ);
4058
4059 // v_r = -v_r = -r <v, r> (where r is normalized)
4060 // => v = v - 2*r <r,v>
4061 creal vNeighboursdotr = (vNeighboursX * RnormX + vNeighboursY * RnormY + vNeighboursZ * RnormZ);
4062 creal vNeighboursMirroredX = vNeighboursX - 2 * RnormX * vNeighboursdotr;
4063 creal vNeighboursMirroredY = vNeighboursY - 2 * RnormY * vNeighboursdotr;
4064 creal vNeighboursMirroredZ = vNeighboursZ - 2 * RnormZ * vNeighboursdotr;
4065 Realf value = 0;
4066 if (vdotr < 0) {
4067 value = projects::MaxwellianPhaseSpaceDensity(vx - vNeighboursX, vy - vNeighboursY, vz - vNeighboursZ, temperature, density, mass);
4068 } else {
4069 if (1 - mu * mu < sqrt(Bsqr) / 5e-5) {
4070 // outside the loss cone
4071 value = projects::MaxwellianPhaseSpaceDensity(vx - 2 * RnormX * vdotr - vNeighboursMirroredX, vy - 2 * RnormY * vdotr - vNeighboursMirroredY, vz - 2 * RnormZ * vdotr - vNeighboursMirroredZ, temperature, density, mass);
4072 } else {
4073 // Inside the loss cone
4074 value = 0;
4075 }
4076 }
4077 // Add ionospheric outflow maxwellian on top.
4078 value += projects::MaxwellianPhaseSpaceDensity(vx - initV0X, vy - initV0Y, vz - initV0Z, initT, initRho, mass);
4079 bufferData[initIndex * WID3 + k * WID2 + j * WID + i] = value;
4080 // lsum[0] += value;
4081 };
4082 },
4083 rhosum);
4084
4085#ifdef USE_GPU
4086 // Set and apply the reservation value
4087 cell.setReservation(popID, nRequested, true); // Force to this value
4088 cell.applyReservation(popID);
4089#endif
4090 } // end case CopyAndLosscone
4091 break;
4092 } // end switch VDF method
4093 // let's get rid of blocks not fulfilling the criteria here to save memory.
4094 mpiGrid[cellID]->adjustSingleCellVelocityBlocks(popID, true);
4095
4096 // In principle this could call _R or _V instead according to calculate_V_moments (unused at the moment)
4097 // But the relevant moments will get recomputed in other spots when needed.
4098 calculateCellMoments(mpiGrid[cellID], true, false, true);
4099 } // End of if for coupling interval, we skip this altogether
4100 }
4101
4107 // WARNING not 0.0 here or the dipole() function fails miserably.
4108 templateCell.sysBoundaryFlag = this->getIndex();
4109 templateCell.sysBoundaryLayer = 1;
4110 templateCell.parameters[CellParams::XCRD] = 1.0;
4111 templateCell.parameters[CellParams::YCRD] = 1.0;
4112 templateCell.parameters[CellParams::ZCRD] = 1.0;
4113 templateCell.parameters[CellParams::DX] = 1;
4114 templateCell.parameters[CellParams::DY] = 1;
4115 templateCell.parameters[CellParams::DZ] = 1;
4116
4117 Real initRho, initT, initV0X, initV0Y, initV0Z;
4118 // Loop over particle species
4119 for (uint popID = 0; popID < getObjectWrapper().particleSpecies.size(); ++popID) {
4120 templateCell.clear(popID, false); // clear, do not de-allocate memory
4121 const IonosphereSpeciesParameters& sP = this->speciesParams[popID];
4122 const Real mass = getObjectWrapper().particleSpecies[popID].mass;
4123 initRho = sP.rho;
4124 initT = sP.T;
4125 initV0X = 0;
4126 initV0Y = 0;
4127 initV0Z = 0;
4128
4129 // Find list of blocks to initialize.
4130 const uint nRequested = SBC::findMaxwellianBlocksToInitialize(popID, templateCell, initRho, initT, initV0X, initV0Y, initV0Z);
4131 // stores in vmesh->getGrid() (localToGlobalMap)
4132 // with count in cell.get_population(popID).N_blocks
4133
4134 // Resize and populate mesh
4135 templateCell.prepare_to_receive_blocks(popID);
4136
4137 // Set the reservation value (capacity is increased in add_velocity_blocks
4138 // const Realf minValue = templateCell.getVelocityBlockMinValue(popID);
4139
4140 // fills v-space into target
4141
4142#ifdef USE_GPU
4143 vmesh::VelocityMesh* vmesh = templateCell.dev_get_velocity_mesh(popID);
4144 vmesh::VelocityBlockContainer* VBC = templateCell.dev_get_velocity_blocks(popID);
4145#else
4146 vmesh::VelocityMesh* vmesh = templateCell.get_velocity_mesh(popID);
4147 vmesh::VelocityBlockContainer* VBC = templateCell.get_velocity_blocks(popID);
4148#endif
4149 // Loop over blocks
4150 Realf rhosum = 0;
4152 {WID, WID, WID, nRequested},
4153 ARCH_LOOP_LAMBDA(const uint i, const uint j, const uint k, const uint initIndex, Realf* lsum) {
4154 vmesh::GlobalID* GIDlist = vmesh->getGrid()->data();
4155 Realf* bufferData = VBC->getData();
4156 const vmesh::GlobalID blockGID = GIDlist[initIndex];
4157 // Calculate parameters for new block
4158 Real blockCoords[6];
4159 vmesh->getBlockInfo(blockGID, &blockCoords[0]);
4160 creal vxBlock = blockCoords[0];
4161 creal vyBlock = blockCoords[1];
4162 creal vzBlock = blockCoords[2];
4163 creal dvxCell = blockCoords[3];
4164 creal dvyCell = blockCoords[4];
4165 creal dvzCell = blockCoords[5];
4166 ARCH_INNER_BODY(i, j, k, initIndex, lsum) {
4167 creal vx = vxBlock + (i + 0.5) * dvxCell - initV0X;
4168 creal vy = vyBlock + (j + 0.5) * dvyCell - initV0Y;
4169 creal vz = vzBlock + (k + 0.5) * dvzCell - initV0Z;
4170 const Realf value = projects::MaxwellianPhaseSpaceDensity(vx, vy, vz, initT, initRho, mass);
4171 bufferData[initIndex * WID3 + k * WID2 + j * WID + i] = value;
4172 // lsum[0] += value;
4173 };
4174 },
4175 rhosum);
4176
4177#ifdef USE_GPU
4178 // Set and apply the reservation value
4179 templateCell.setReservation(popID, nRequested, true); // Force to this value
4180 templateCell.applyReservation(popID);
4181#endif
4182
4183 // let's get rid of blocks not fulfilling the criteria here to save memory.
4184 templateCell.adjustSingleCellVelocityBlocks(popID, true);
4185 } // for-loop over particle species
4186
4187 calculateCellMoments(&templateCell, true, false, true);
4188
4189 // WARNING Time-independence assumed here. Normal moments computed in setProjectCell
4191 templateCell.parameters[CellParams::VX_R] = templateCell.parameters[CellParams::VX];
4192 templateCell.parameters[CellParams::VY_R] = templateCell.parameters[CellParams::VY];
4193 templateCell.parameters[CellParams::VZ_R] = templateCell.parameters[CellParams::VZ];
4199 templateCell.parameters[CellParams::VX_V] = templateCell.parameters[CellParams::VX];
4200 templateCell.parameters[CellParams::VY_V] = templateCell.parameters[CellParams::VY];
4201 templateCell.parameters[CellParams::VZ_V] = templateCell.parameters[CellParams::VZ];
4206 }
4207
4208 void Ionosphere::setCellFromTemplate(SpatialCell* cell, const uint popID) {
4209 copyCellData(&templateCell, cell, false, popID, true); // copy also vdf, _V
4210 copyCellData(&templateCell, cell, true, popID, false); // don't copy vdf again but copy _R now
4211#ifdef USE_GPU
4212 cell->setReservation(popID, templateCell.getReservation(popID));
4213#endif
4214 }
4215
4216 std::string Ionosphere::getName() const { return "Ionosphere"; }
4217 void Ionosphere::getFaces(bool* faces) {}
4218
4219 void Ionosphere::updateState(dccrg::Dccrg<SpatialCell, dccrg::Cartesian_Geometry>& mpiGrid, fsgrids::technicalspan technical, FieldSolverGrid& fsgrid, fsgrids::perbspan perb, fsgrids::bgbspan bgb, creal t) {}
4220
4222} // namespace SBC
density
Definition Dispersion.m:42
for i
Definition Dispersion.m:24
dx
Definition Dispersion.m:38
sqrt(1.0+vA *vA/(c *c))) % Ion-acoustic wave cS
Parameters length
Definition Dispersion.m:36
Constants c
Definition Dispersion.m:45
#define ARCH_INNER_BODY(...)
#define ARCH_LOOP_LAMBDA
void calculateCellMoments(spatial_cell::SpatialCell *cell, const bool &computeSecond, const bool &computePopulationMomentsOnly, const bool &doNotSkip)
static void addComposing(const std::string &name, const std::string &desc)
static void get(const std::string &name, std::string &value)
static void add(const std::string &name, const std::string &desc, const std::string &defValue)
virtual void initSysBoundary(creal &t, Project &project) override
static Real innerRadius
Definition ionosphere.h:622
static enum SBC::Ionosphere::downmapSamplingMode downmapFACsamplingMode
virtual void applyInitialState(dccrg::Dccrg< SpatialCell, dccrg::Cartesian_Geometry > &mpiGrid, fsgrids::technicalspan technical, FieldSolverGrid &fsgrid, fsgrids::perbspan perb, fsgrids::bgbspan bgb, Project &project) override
static void addParameters()
static bool useEigenSolver
Definition ionosphere.h:623
std::string path
Definition ionosphere.h:673
static std::vector< IonosphereSpeciesParameters > speciesParams
Definition ionosphere.h:619
static Real unmappedNodeRho
Definition ionosphere.h:645
virtual std::string getName() const override
virtual Real fieldSolverBoundaryCondMagneticField(fsgrids::perbspan b, fsgrids::constbgbspan bgb, fsgrids::consttechnicalspan technical, const std::array< Real, 3 > &gridSpacing, const std::array< fsgrid::FsSize_t, 3 > &globalCoordinates, const fsgrid::FsStencil &stencil, cuint component) override
static Real solverRelativeL2ConvergenceThreshold
Definition ionosphere.h:625
virtual uint getIndex() const override
static Real downmapRadius
Definition ionosphere.h:638
static Real shieldingLatitude
Definition ionosphere.h:631
virtual void getParameters() override
virtual void mapCellPotentialAndGetEXBDrift(std::array< Real, CellParams::N_SPATIAL_CELL_PARAMS > &cellParams) override
virtual void updateState(dccrg::Dccrg< SpatialCell, dccrg::Cartesian_Geometry > &mpiGrid, fsgrids::technicalspan technical, FieldSolverGrid &fsgrid, fsgrids::perbspan perb, fsgrids::bgbspan bgb, creal t) override
virtual void fieldSolverBoundaryCondElectricField(fsgrids::efieldspan e, const fsgrid::FsStencil &stencil, cuint component) override
virtual void getFaces(bool *faces) override
static int solverMaxFailureCount
Definition ionosphere.h:626
static int solveCount
Definition ionosphere.h:649
std::string atmosphericModelFile
Definition ionosphere.h:677
static Real unmappedNodeTe
Definition ionosphere.h:646
static bool solverPreconditioning
Definition ionosphere.h:628
std::vector< Real > refineMaxLatitudes
Definition ionosphere.h:681
virtual void assignSysBoundary(dccrg::Dccrg< SpatialCell, dccrg::Cartesian_Geometry > &mpiGrid, fsgrids::technicalspan technical, FieldSolverGrid &fsgrid) override
static Real couplingInterval
Definition ionosphere.h:648
static Real backgroundIonisation
Definition ionosphere.h:637
spatial_cell::SpatialCell templateCell
Definition ionosphere.h:683
static Real ridleyParallelConductivity
Definition ionosphere.h:632
static Real recombAlpha
Definition ionosphere.h:635
static Real downmapSamplingWidth
Definition ionosphere.h:639
std::array< Real, 3 > fieldSolverGetNormalDirection(fsgrids::technicalspan technical, FieldSolverGrid &fsgrid, cint i, cint j, cint k)
static Real F10_7
Definition ionosphere.h:636
std::string baseShape
Definition ionosphere.h:672
static int solverMaxIterations
Definition ionosphere.h:624
static Real couplingTimescale
Definition ionosphere.h:647
std::vector< Real > refineMinLatitudes
Definition ionosphere.h:680
static Real radius
Definition ionosphere.h:618
virtual void vlasovBoundaryCondition(dccrg::Dccrg< SpatialCell, dccrg::Cartesian_Geometry > &mpiGrid, const CellID &cellID, const uint popID, const bool calculate_V_moments) override
virtual ~Ionosphere()
void setCellFromTemplate(SpatialCell *cell, const uint popID)
static bool solverToggleMinimumResidualVariant
Definition ionosphere.h:630
virtual void fieldSolverBoundaryCondDerivatives(fsgrids::dperbspan dperb, fsgrids::dmomentsspan dmoments, const fsgrid::FsStencil &stencil, cuint RKCase, cuint component) override
static Real fixedSigmaH
Definition ionosphere.h:656
static enum SBC::Ionosphere::IonosphereConductivityModel conductivityModel
virtual void fieldSolverBoundaryCondBVOLDerivatives(fsgrids::volspan vols, const fsgrid::FsStencil &stencil, cuint component) override
Real earthAngularVelocity
Definition ionosphere.h:675
virtual void fieldSolverBoundaryCondHallElectricField(fsgrids::ehallspan ehall, const fsgrid::FsStencil &stencil, cuint component) override
static bool solverUseMinimumResidualVariant
Definition ionosphere.h:629
static Real fixedSigmaP
Definition ionosphere.h:655
virtual void fieldSolverBoundaryCondGradPeElectricField(fsgrids::egradpespan EGradPe, const fsgrid::FsStencil &stencil, cuint component) override
static Real solverMaxErrorGrowthFactor
Definition ionosphere.h:627
static void setCellBVOLDerivativesToZero(fsgrids::volspan vols, const fsgrid::FsStencil &stencil, cuint component)
std::vector< std::array< int, 3 > > getAllClosestNonsysboundaryCells(fsgrids::technicalspan technical, FieldSolverGrid &fsgrid, cint i, cint j, cint k)
void copyCellData(const SpatialCell *from, SpatialCell *to, const bool copyMomentsOnly, const uint popID, const bool copy_V_moments)
static void setCellDerivativesToZero(fsgrids::dperbspan dperb, fsgrids::dmomentsspan dmoments, const fsgrid::FsStencil &stencil, cuint component)
bool checkMesh(const uint popID)
vmesh::VelocityMesh * get_velocity_mesh(const size_t &popID)
vmesh::VelocityBlockContainer * get_velocity_blocks(const size_t &popID)
void clear(const uint popID, bool shrink=false)
void prepare_to_receive_blocks(const uint popID)
void applyReservation(const uint popID)
std::array< Real, CellParams::N_SPATIAL_CELL_PARAMS > parameters
void setReservation(const uint popID, const vmesh::LocalID reservationsize, bool force=false)
vmesh::VelocityBlockContainer * dev_get_velocity_blocks(const size_t &popID)
vmesh::VelocityMesh * dev_get_velocity_mesh(const size_t &popID)
const std::vector< CellID > & getLocalCells()
Definition main.cpp:39
@ SOURCE
Definition common.h:459
@ SIGMAP
Definition common.h:464
@ SOLUTION
Definition common.h:472
@ PPARAM
Definition common.h:477
@ NODE_BX
Definition common.h:470
@ BEST_SOLUTION
Definition common.h:473
@ SIGMAPARALLEL
Definition common.h:466
@ PPPARAM
Definition common.h:477
@ RESIDUAL
Definition common.h:474
@ ZZPARAM
Definition common.h:476
@ N_IONOSPHERE_PARAMETERS
Definition common.h:478
@ UPMAPPED_BX
Definition common.h:471
@ ZPARAM
Definition common.h:476
@ NODE_BY
Definition common.h:470
@ RRESIDUAL
Definition common.h:475
@ SIGMAH
Definition common.h:465
@ UPMAPPED_BZ
Definition common.h:471
@ PRECIP
Definition common.h:467
@ TEMPERATURE
Definition common.h:469
@ RHON
Definition common.h:468
@ SIGMA
Definition common.h:460
@ UPMAPPED_BY
Definition common.h:471
@ NODE_BZ
Definition common.h:470
#define WID
Definition common.h:514
const int WID3
Definition common.h:517
const int WID2
Definition common.h:516
Parameters P
const uint32_t cuint
Definition definitions.h:50
float Real
Definition definitions.h:41
const int cint
Definition definitions.h:45
uint64_t CellID
Definition definitions.h:54
T convert(const T &number)
Definition definitions.h:56
float Realf
Definition definitions.h:33
fsgrid::FsGrid< FS_STENCIL_WIDTH > FieldSolverGrid
Definition definitions.h:78
const float creal
Definition definitions.h:42
std::array< Real, productionNumParticleEnergies+1 > particle_energy
std::array< Real, productionNumParticleEnergies > differentialFlux
#define normalize_vector(v)
#define Vec3d
#define dot_product(av, bv)
#define cross_product(av, bv)
#define vector_length(v)
std::array< Real, 3 > getFractionalFsGridCellForCoord(T &grid, const std::array< Real, 3 > &x)
std::array< fsgrid::FsIndex_t, 3 > getLocalFsGridCellIndexForCoord(T &grid, const std::array< Real, 3 > &x)
Real divideIfNonZero(creal numerator, creal denominator)
Helper function.
Definition fs_common.cpp:33
std::array< Real, 3 > interpolateCurlB(fsgrids::perbspan perb, fsgrids::constdperbspan dperb, fsgrids::technicalspan technical, FieldSolverGrid &fsgrid, std::map< std::array< int, 3 >, std::array< Real, Rec::N_REC_COEFFICIENTS > > &reconstructionCoefficientsCache, cint i, cint j, cint k, const std::array< Real, 3 > x)
@ Y
Definition functions.hpp:28
@ X
Definition functions.hpp:28
@ Z
Definition functions.hpp:28
const Real mu
const int j
const int k
Hardcoded lookup- and interpolation tables for semiempirical ionosphere model implementations.
static const Real c2h
static const Real c2p
static const Real c4P_values[]
static const Real chapman_euv_table[1201]
static const Real c3h
static const Real c1h
static const Real c3p
static const Real c4H_values[]
static const Real c5P_values[]
static const Real c1p
static const Real c5H_values[]
std::function< Real(Real)> c5P
Definition main.cpp:280
std::function< Real(Real)> c4P
Definition main.cpp:273
ObjectWrapper & getObjectWrapper()
Definition main.cpp:33
std::function< Real(Real)> c5H
Definition main.cpp:295
std::function< Real(Real)> c4H
Definition main.cpp:287
Real altcos(Real sza)
Definition main.cpp:310
#define index(i, j, k)
@ BULKV_FORCING_X
Definition common.h:225
@ BULKV_FORCING_Z
Definition common.h:227
@ BULKV_FORCING_Y
Definition common.h:226
FieldTracingParameters fieldTracingParameters
std::array< std::pair< int, Real >, 3 > calculateIonosphereVlasovGridCoupling(std::array< Real, 3 > x, std::vector< SBC::SphericalTriGrid::Node > &nodes, creal couplingRadius)
vmesh::LocalID findMaxwellianBlocksToInitialize(const uint popID, spatial_cell::SpatialCell &cell, creal &rho, creal &T, creal &VX0, creal &VY0, creal &VZ0)
SBC::findMaxwellianBlocksToInitialize returns a list of blocks to construct the VDF with.
IonosphereBoundaryVDFmode
Definition ionosphere.h:62
@ AverageMoments
Definition ionosphere.h:64
@ FixedMoments
Definition ionosphere.h:63
@ CopyAndLosscone
Definition ionosphere.h:66
@ AverageAllMoments
Definition ionosphere.h:65
static constexpr Real productionMinAccEnergy
Definition ionosphere.h:49
static Real SergienkoIvanovLambda(Real E0, Real Chi)
static constexpr Real ion_electron_T_ratio
Definition ionosphere.h:53
static constexpr Real productionMaxAccEnergy
Definition ionosphere.h:50
static constexpr int productionNumAccEnergies
Definition ionosphere.h:46
static constexpr int productionNumParticleEnergies
Definition ionosphere.h:48
static constexpr Real productionMaxTemperature
Definition ionosphere.h:52
SphericalTriGrid ionosphereGrid
static constexpr Real productionMinTemperature
Definition ionosphere.h:51
Real iSolverReal
Definition ionosphere.h:73
IonosphereBoundaryVDFmode boundaryVDFmode
static const int MAX_DEPENDING_NODES
Definition ionosphere.h:71
Real getR(creal x, creal y, creal z, uint geometry, Real center[3])
static constexpr int productionNumTemperatures
Definition ionosphere.h:47
static const int MAX_TOUCHING_ELEMENTS
Definition ionosphere.h:70
static Real ReesIsotropicLambda(Real x)
static void parallel_reduce(const uint(&limits)[NDim], Lambda loop_body, T &sum)
std::span< std::array< Real, fsgrids::bfield::N_BFIELD > > perbspan
Definition common.h:434
@ EXGRADPE
Definition common.h:305
std::span< const std::array< Real, bgbfield::N_BGB > > constbgbspan
Definition common.h:445
std::span< std::array< Real, fsgrids::moments::N_MOMENTS > > momentsspan
Definition common.h:446
std::span< std::array< Real, fsgrids::egradpe::N_EGRADPE > > egradpespan
Definition common.h:440
std::span< std::array< Real, fsgrids::dmoments::N_DMOMENTS > > dmomentsspan
Definition common.h:448
@ P_22
Definition common.h:318
@ P_33
Definition common.h:319
@ P_11
Definition common.h:317
@ RHOQ
Definition common.h:313
std::span< technical > technicalspan
Definition common.h:452
@ EZHALL_010_011
Definition common.h:295
@ EYHALL_101_111
Definition common.h:299
@ EYHALL_100_110
Definition common.h:292
@ EXHALL_010_110
Definition common.h:294
@ EZHALL_110_111
Definition common.h:296
@ EZHALL_000_001
Definition common.h:291
@ EYHALL_001_011
Definition common.h:298
@ EXHALL_001_101
Definition common.h:297
@ EYHALL_000_010
Definition common.h:290
@ EXHALL_000_100
Definition common.h:289
@ EXHALL_011_111
Definition common.h:300
@ EZHALL_100_101
Definition common.h:293
std::span< std::array< Real, bgbfield::N_BGB > > bgbspan
Definition common.h:444
std::span< std::array< Real, fsgrids::dperb::N_DPERB > > dperbspan
Definition common.h:442
std::span< const technical > consttechnicalspan
Definition common.h:453
std::span< std::array< Real, fsgrids::efield::N_EFIELD > > efieldspan
Definition common.h:436
@ PERBX
Definition common.h:275
std::span< std::array< Real, fsgrids::volfields::N_VOL > > volspan
Definition common.h:450
std::span< std::array< Real, fsgrids::ehall::N_EHALL > > ehallspan
Definition common.h:438
std::span< const std::array< Real, fsgrids::dperb::N_DPERB > > constdperbspan
Definition common.h:443
const Real CHARGE
Definition common.h:572
const Real K_B
Definition common.h:571
const Real MU_0
Definition common.h:570
const Real MASS_ELECTRON
Definition common.h:573
const Real R_E
Definition common.h:575
const Real MASS_PROTON
Definition common.h:574
ARCH_HOSTDEV Realf MaxwellianPhaseSpaceDensity(creal &vx, creal &vy, creal &vz, creal &T, creal &rho, creal &mass)
Definition project.h:45
uint32_t GlobalID
Definition definitions.h:59
static const Real recombAlpha
std::vector< species::Species > particleSpecies
static uint zcells_ini
Definition parameters.h:50
static Real ymin
Definition parameters.h:40
static uint ycells_ini
Definition parameters.h:49
static uint xcells_ini
Definition parameters.h:48
static Real xmin
Definition parameters.h:38
static Real zmin
Definition parameters.h:42
static Real dt
Definition parameters.h:55
std::array< uint32_t, 3 > corners
Definition ionosphere.h:81
std::array< iSolverReal, N_IONOSPHERE_PARAMETERS > parameters
Definition ionosphere.h:102
std::array< uint32_t, MAX_TOUCHING_ELEMENTS > touchingElements
Definition ionosphere.h:91
std::array< Real, 3 > x
Definition ionosphere.h:99
std::array< Real, 9 > sigmaAverage(uint elementIndex)
void mapDownBoundaryData(fsgrids::perbspan perb, fsgrids::constdperbspan dperb, fsgrids::momentsspan moments, fsgrids::technicalspan technical, FieldSolverGrid &fsgrid)
void initializeSphericalFibonacci(int n)
Real lookupProductionValue(int heightindex, Real energy_keV, Real temperature_keV)
std::array< Real, 3 > BGB
Definition ionosphere.h:179
std::array< std::array< std::array< Real, productionNumTemperatures >, productionNumAccEnergies >, numAtmosphereLevels > productionTable
Definition ionosphere.h:170
std::array< Real, 3 > computeGradT(const std::array< Real, 3 > &a, const std::array< Real, 3 > &b, const std::array< Real, 3 > &c)
Real elementArea(uint32_t elementIndex)
Definition ionosphere.h:243
uint32_t findNodeAtCoordinates(std::array< Real, 3 > x)
Real interpolateUpmappedPotential(const std::array< Real, 3 > &x)
std::map< std::array< Real, 3 >, std::array< std::pair< int, Real >, 3 > > vlasovGridCoupling
Definition ionosphere.h:182
FieldFunction dipoleField
Definition ionosphere.h:178
Eigen::Vector3d commonEdgeMidpoint(uint32_t el1, uint32_t el2)
Definition ionosphere.h:387
void normalizeRadius(Node &n, Real R)
void readAtmosphericModelFile(const char *filename)
void calculateConductivityTensor(const Real F10_7, const Real recombAlpha, const Real backgroundIonisation, const bool refillTensorAtRestart=false)
void initSolver(bool zeroOut=true)
void initializeGridFromFile(std::string path)
std::vector< Eigen::Vector3d > elementDivFreeCurrent
Definition ionosphere.h:86
void addAllMatrixDependencies(uint nodeIndex)
void subdivideElement(uint32_t e)
int32_t findElementNeighbour(uint32_t e, int n1, int n2)
void updateIonosphereCommunicator(dccrg::Dccrg< SpatialCell, dccrg::Cartesian_Geometry > &mpiGrid, fsgrids::technicalspan technical, FieldSolverGrid &fsgrid)
Real areaInDualPolygon(uint gridNode, uint gridElem)
Definition ionosphere.h:457
std::vector< Node > nodes
Definition ionosphere.h:137
Eigen::Vector3d elementNormal(uint32_t el)
Definition ionosphere.h:361
void solveInternal(int &iteration, int &nRestarts, Real &residual, Real &minPotentialN, Real &maxPotentialN, Real &minPotentialS, Real &maxPotentialS)
iSolverReal Atimes(uint nodeIndex, int parameter, bool transpose=false)
Eigen::Vector3d elementCircumcentre(uint el)
Definition ionosphere.h:312
std::vector< Element > elements
Definition ionosphere.h:84
enum SBC::SphericalTriGrid::IonosphereIonizationModel ionizationModel
std::array< AtmosphericLayer, numAtmosphereLevels > atmosphere
Definition ionosphere.h:151
Real Asolve(uint nodeIndex, int parameter, bool transpose=false)
static constexpr int numAtmosphereLevels
Definition ionosphere.h:140
std::vector< Eigen::Vector3d > elementCurlFreeCurrent
Definition ionosphere.h:85
double elementIntegral(uint elementIndex, int i, int j, bool transpose=false)
void solve(int &iteration, int &nRestarts, Real &residual, Real &minPotentialN, Real &maxPotentialN, Real &minPotentialS, Real &maxPotentialS)
void addMatrixDependency(uint node1, uint node2, Real coeff, bool transposed=false)
static bool ionosphereJustSolved
Definition common.h:543
static ARCH_HOSTDEV VecSimple< T > min(VecSimple< T > const &l, VecSimple< T > const &r)
static ARCH_HOSTDEV VecSimple< T > max(VecSimple< T > const &l, VecSimple< T > const &r)
static ARCH_HOSTDEV VecSimple< T > abs(const VecSimple< T > &l)
static ARCH_HOSTDEV VecSimple< T > floor(VecSimple< T > const &a)