API reference¶
Auto-generated from source code. See the user guide for usage examples.
Core¶
DosimetryEngine¶
aegis.engine.DosimetryEngine ¶
Compute absorbed power density on a body mesh from propagation paths.
Parameters¶
tissue : TissueModel Tissue electromagnetic properties at the operating frequency.
Source code in src/aegis/engine.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 | |
compute ¶
compute(body: BodyMesh, paths: PropagationPaths, level: int | None = None, body_mass: float | None = None, spatial_averaging: bool = True, A_ab: float | None = None, D_max: float | None = None, sh_coeffs: ndarray | None = None, sh_L: int = 4, D_table: ndarray | None = None, D_dirs: ndarray | None = None, q: ndarray | float = 0.0, curvature_H: ndarray | None = None, occlusion: ndarray | None = None, precoder: Precoder | None = None, h: ndarray | None = None, P_abs_max: float = DEFAULT_P_ABS_MAX, mode: str | None = None, fresnel: bool = True, polarisation: bool = False, diffraction: bool | None = None, diffraction_model: str | None = None, inter_body: str = 'off', curvature: bool = False, self_shadow: bool = False, self_shadow_directional: bool = False, source_pos: ndarray | None = None, vis_resolution: int = 32, freq_hz: float | None = None, _timings: dict[str, float] | None = None) -> DosimetryResult
Compute dosimetry at the specified fidelity level or mode.
Parameters¶
body : BodyMesh paths : PropagationPaths level : fidelity level 0-8 (legacy API, mutually exclusive with mode) body_mass : body mass [kg] for SAR computation spatial_averaging : compute ICNIRP 4 cm^2 spatial averaging (default True) A_ab : absorption area [m^2] (required for levels 0-1) D_max : max directivity (required for level 0) sh_coeffs : SH coefficients for D(k_hat) (level 1) sh_L : SH degree (level 1) D_table : directivity LUT (level 1 alternative) D_dirs : directions for D_table (level 1 alternative) q : TM excess (level 4 or mode='spatial' with polarisation=True) curvature_H : (M,) twice mean curvature [1/m] (levels 5-6 or curvature/diffraction flags) precoder : Precoder with precoding vector x (required for level 7) h : (M_ant,) UE channel vector (required for level 8, optional for 7) P_abs_max : maximum absorbed power [W] (level 8) mode : one of 'bound', 'aggregate', 'spatial', 'coherent', 'ecbf' fresnel : use angle-dependent Fresnel (spatial mode, default True) polarisation : enable polarisation correction (spatial mode) diffraction : legacy bool. At the engine layer True -> "fock", False -> "none". An explicit diffraction_model always wins. None (the default) leaves the model at its "fock" default. diffraction_model : shadow-edge gate "none" | "gelu" | "fock". Default "fock". Levels 0-5 have no shadow gate and ignore diffraction_model; it applies to spatial mode, level 6, and coherent levels 7-8. inter_body : "off" (default) or "specular1". "specular1" adds one specular recapture bounce (off-by-default, single bounce): each lit triangle's Fresnel-reflected ray is cast through the visibility BVH and, when it strikes another body triangle, the recaptured power is deposited there. Applies to spatial mode and legacy levels >= 2. curvature : enable curvature correction (spatial mode) self_shadow : enable distal self-shadowing (one body part shadowing another) via the baked per-body visibility LUT and the distal Fock gate. Default False (opt-in): turning it on changes the dose for non-convex bodies and breaks the spatial==level-N composability invariant, so it is off until explicitly requested. Convex bodies short-circuit to a no-op. Applies to spatial mode, level 6, and coherent levels 7-8; ignored when an explicit occlusion override is supplied. source_pos : (3,) point-source position for near-field self-shadowing. None (default) uses the far-field path directions in paths.k_hat. vis_resolution : octahedral LUT resolution for the self-shadow bake. _timings : if provided, fine-grained timing data is written into this dict
Returns¶
DosimetryResult
Source code in src/aegis/engine.py
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 | |
compute_with_timings ¶
Like compute(), but returns a (DosimetryResult, timings) tuple.
The timings dict is local to this call (thread-safe). Keys match those in _last_timings: kernel_ms, avg_build_G_4cm2_ms, avg_matvec_4cm2_ms, avg_build_G_1cm2_ms.
Parameters mirror compute() exactly.
Source code in src/aegis/engine.py
compute_sab ¶
compute_sab(body: BodyMesh, paths: PropagationPaths, level: int | None = None, *, precoder_x=None, precoder: Precoder | None = None, h=None, P_abs_max: float = DEFAULT_P_ABS_MAX, A_ab: float | None = None, D_max: float | None = None, sh_coeffs=None, sh_L: int = 4, D_table=None, D_dirs=None, q: float = 0.0, curvature_H=None, mode: str | None = None, fresnel: bool = True, polarisation: bool = False, diffraction: bool | None = None, diffraction_model: str | None = None, inter_body: str = 'off', curvature: bool = False, freq_hz: float | None = None)
Return per-triangle S_ab as a raw array (JAX or NumPy).
Unlike compute(), this does not convert to NumPy or wrap in DosimetryResult. Use inside jax.grad boundaries for differentiable optimization. The diffraction-model resolution mirrors compute(), so compute_sab stays numerically consistent with compute().sab (default "fock"). The Fock radius is a geometry constant, so threading it does not break autodiff w.r.t. the precoder or source. inter_body mirrors compute()'s validation contract but is otherwise ignored here: the single specular recapture is a ray-cast pass that is not differentiable, so it does not apply to the autodiff path.
Source code in src/aegis/engine.py
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 | |
sweep_levels ¶
sweep_levels(body: BodyMesh, paths: PropagationPaths, levels: list[int] | None = None, *, body_mass: float | None = None, spatial_averaging: bool = True, A_ab: float | None = None, D_max: float | None = None, q: ndarray | float = 0.0, curvature_H: ndarray | None = None, freq_hz: float | None = None) -> dict[int, DosimetryResult]
Compute dosimetry at multiple fidelity levels for convergence analysis.
Runs each requested level and returns a dict mapping level -> result. Levels that require unavailable parameters are silently skipped.
Parameters¶
body : BodyMesh paths : PropagationPaths levels : list of ints, or None for all feasible incoherent levels body_mass : body mass [kg] for SAR (optional) A_ab : absorption area for levels 0-1 D_max : max directivity for level 0 q : TM excess for level 4 curvature_H : mean curvature for levels 5-6 freq_hz : frequency override
Returns¶
dict mapping int level -> DosimetryResult
Source code in src/aegis/engine.py
1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 | |
DosimetryResult¶
aegis.result.DosimetryResult dataclass ¶
Output of a dosimetry computation.
Attributes¶
sab : (M,) per-triangle absorbed power density [W/m^2] sab_averaged : (M,) or None, spatially averaged (ICNIRP 4 cm^2) [W/m^2] p_abs : total absorbed power [W] sar_wb : whole-body SAR [W/kg], or None if body mass not provided fidelity_level : which kernel level produced this result (0-8)
Source code in src/aegis/result.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 | |
peak_sab_averaged property ¶
Peak spatially averaged S_ab [W/m^2], or None if not computed.
compliant_sab property ¶
ICNIRP compliance: peak spatially averaged S_ab <= limit.
Returns None if freq_hz is not set or outside the ICNIRP 2020 range (>6 GHz to 300 GHz).
compliant_sar property ¶
ICNIRP compliance for whole-body SAR: <= limit.
Returns None if SAR was not computed.
to_dict ¶
Serialize fields to a JSON-friendly dict. Omits None values.
Complex arrays (Q, eigenvalues, x_star) are serialized as {"real": [...], "imag": [...]}.
Source code in src/aegis/result.py
from_dict classmethod ¶
Reconstruct a DosimetryResult from a dict (inverse of to_dict).
Complex arrays serialized as {"real": [...], "imag": [...]} are reconstructed as complex numpy arrays. Real lists become float64 arrays. Scalar fields are passed through.
Source code in src/aegis/result.py
from_json classmethod ¶
scale ¶
Return a new result with all power quantities scaled by factor.
S_ab is linear in transmit power for all fidelity levels (0-8). This enables parameter sweeps: compute once at a reference power, then scale to explore the compliance boundary.
Coherent-specific fields (Q, eigenvalues) scale with factor too, since Q ~ P and eigenvalues are eigenvalues of Q. The precoder x_star is not scaled (it encodes direction, not magnitude).
Parameters¶
factor : float Multiplicative scaling factor. Must be non-negative.
Source code in src/aegis/result.py
compliance_kwargs ¶
Extract all compliance quantities as kwargs for evaluate_compliance.
Parameters¶
body : BodyMesh or None If provided, computes sinc_whole_body (area-weighted mean S_inc).
Source code in src/aegis/result.py
evaluate_compliance ¶
Run a full ICNIRP 2020 compliance evaluation on this result.
Source code in src/aegis/result.py
compare staticmethod ¶
Compare multiple dosimetry results (e.g. across fidelity levels).
Produces a summary dict with per-result metrics and pairwise relative errors. Useful for fidelity level convergence studies.
Parameters¶
results : dict mapping label -> DosimetryResult At least two results required. Labels can be anything (e.g. "level2", "level3", "spatial+fresnel+pol").
Returns¶
dict with keys: labels : list of result labels peak_sab : dict of label -> peak S_ab [W/m^2] p_abs : dict of label -> total absorbed power [W] peak_sab_averaged : dict of label -> peak averaged S_ab or None relative_error : dict of (label_i, label_j) -> relative error in peak S_ab rmse : dict of (label_i, label_j) -> RMSE of per-triangle S_ab max_abs_error : dict of (label_i, label_j) -> max absolute error
Source code in src/aegis/result.py
show ¶
Render S_ab as a heatmap on the body mesh.
Convenience wrapper around aegis.viz.plot_heatmap. Requires aegis[viz] (matplotlib or plotly).
Parameters¶
body : BodyMesh The body mesh used to compute this result. averaged : bool If True, plot spatially averaged S_ab instead of raw per-triangle values. **kwargs Forwarded to plot_heatmap (title, cmap, backend, show, out_path, etc.).
Source code in src/aegis/result.py
PropagationPaths¶
aegis.paths.PropagationPaths dataclass ¶
Batch of N propagation paths arriving at the body.
Attributes¶
k_hat : (N, 3) unit directions of arrival psi : (N, 3) complex polarisation-amplitude vectors (V/m / sqrt(W)) element_index : (N,) originating antenna element index delay : (N,) propagation delay in seconds (optional metadata) is_los : (N,) line-of-sight flag (optional metadata) k_hat_tx : optional (N, 3) unit departure directions at the source. For a bounced path this differs from k_hat: array steering and element patterns act on the departure direction, body-side physics on arrival. None when the producer cannot supply it (consumers fall back to k_hat, exact for LOS).
Source code in src/aegis/paths.py
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
power property ¶
Per-path incident power density S_i = |psi_i|^2 / (2 * Z_0) [W/m^2].
The factor 1/(2*Z_0) converts from |E|^2 to power density for a plane wave.
subset ¶
Return paths restricted to the given index array (e.g. LOS-only).
Source code in src/aegis/paths.py
from_powers classmethod ¶
from_powers(k_hat: ndarray, power: ndarray, polarisation: ndarray | None = None) -> PropagationPaths
Construct from directions and scalar powers (incoherent use).
Parameters¶
k_hat : (N, 3) incident directions (will be normalised) power : (N,) incident power density per path [W/m^2] polarisation : optional (3,) or (N, 3) incident E-field direction. When given, the field is projected onto the plane transverse to each k_hat and the result is flagged polarised=True so polarisation-aware kernels use it. Real or complex (elliptical). When None (default) an arbitrary perpendicular polarisation is fabricated and the result is flagged polarised=False so incoherent dosimetry treats the field as unpolarised.
Source code in src/aegis/paths.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
from_spherical classmethod ¶
Construct from spherical arrival angles and scalar powers.
Convenient for analytical scenarios (uniform illumination, sector beams, stochastic channel models) where paths are specified as (theta, phi) rather than Cartesian k_hat.
Parameters¶
theta : (N,) zenith angle of arrival in radians (0 = +z) phi : (N,) azimuth angle of arrival in radians power : (N,) incident power density per path [W/m^2]
Source code in src/aegis/paths.py
uniform_sphere classmethod ¶
Generate paths uniformly distributed over the sphere.
Useful for worst-case analysis, Monte Carlo integration of the exposure integral, and testing. Each path carries equal power such that the total incident power density sums to total_power.
Parameters¶
n_paths : int Number of paths to generate. total_power : float Total incident power density [W/m^2], distributed equally. seed : int or None Random seed for reproducibility.
Source code in src/aegis/paths.py
concatenate classmethod ¶
concatenate(paths_list: Sequence[PropagationPaths], *, reindex_elements: bool = True) -> PropagationPaths
Concatenate multiple PropagationPaths into one.
Parameters¶
paths_list : sequence of PropagationPaths Paths to concatenate. Empty entries are skipped. reindex_elements : bool If True (default), shift element_index so that each input's elements are disjoint. If False, keep element indices as-is (useful when paths already share a common antenna indexing).
Returns¶
PropagationPaths Combined paths with N = sum(N_i) total paths.
Source code in src/aegis/paths.py
to_dict ¶
Serialize to a JSON-friendly dict.
Complex arrays (psi) are stored as {"real": [...], "imag": [...]}.
Source code in src/aegis/paths.py
from_dict classmethod ¶
Reconstruct from a dict (inverse of to_dict).
Source code in src/aegis/paths.py
Precoder¶
aegis.precoder.Precoder dataclass ¶
Precoding vector for coherent MIMO transmission.
Attributes¶
x : (M_ant,) complex precoding vector. ||x||^2 = total transmit power P.
Source code in src/aegis/precoder.py
mrt classmethod ¶
Maximum ratio transmission: x = sqrt(P) * h* / ||h||.
Parameters¶
h : (M_ant,) UE channel vector. P : float Total transmit power [W].
Source code in src/aegis/precoder.py
ecbf classmethod ¶
Exposure-constrained beamforming via QCQP.
Parameters¶
h : (M_ant,) UE channel vector. Q : (M_ant, M_ant) Exposure operator. P_abs_max : float Maximum allowed absorbed power [W]. P : float Total transmit power [W].
Source code in src/aegis/precoder.py
Tissue¶
TissueModel¶
aegis.tissue.dielectric.TissueModel dataclass ¶
Tissue electromagnetic properties at a specific frequency.
Parameters¶
name Human-readable label (e.g. "Skin 28 GHz"). eps_r Relative permittivity. sigma Conductivity (S/m). freq_hz Frequency (Hz).
Source code in src/aegis/tissue/dielectric.py
from_params classmethod ¶
Construct from explicit electromagnetic parameters.
plot_spectrum ¶
plot_spectrum(freq_min_hz: float = 1000000000.0, freq_max_hz: float = 100000000000.0, *, n_points: int = 200)
Plot permittivity and conductivity vs frequency for this tissue.
Uses the IT'IS Cole-Cole database. The tissue name (first word of self.name) is used as the database lookup key.
Requires aegis[viz] (matplotlib).
Parameters¶
freq_min_hz : float Lower frequency bound [Hz]. freq_max_hz : float Upper frequency bound [Hz]. n_points : int Number of frequency samples.
Source code in src/aegis/tissue/dielectric.py
from_database classmethod ¶
Construct from the IT'IS v5.0 database using the Cole-Cole model.
Parameters¶
tissue_name Tissue name in the database (e.g. "Skin", "Muscle"). freq_hz Frequency (Hz). db_path Explicit path to itis_v5.db. Auto-detected if None.
Source code in src/aegis/tissue/dielectric.py
Fresnel coefficients¶
aegis.tissue.fresnel ¶
Fresnel power transmission coefficients for lossy dielectric half-spaces.
Numerically stable implementation using energy conservation: T = 1 - |r|^2. The square-root branch for xi is selected so Re(xi) >= 0.
n_complex ¶
Complex refractive index. Scalar-only, not JIT-traced.
Source code in src/aegis/tissue/fresnel.py
fresnel_transmission ¶
Fresnel power transmission for TE and TM polarizations.
Convenience wrapper with scalar support. NOT called from JIT boundaries. JIT'd kernels use _fresnel_core via _base.py::fresnel_weights instead.
Source code in src/aegis/tissue/fresnel.py
fresnel_reflection ¶
Fresnel amplitude reflection coefficients.
Convenience wrapper with scalar support. NOT called from JIT boundaries.
Source code in src/aegis/tissue/fresnel.py
fresnel_amplitude ¶
Fresnel amplitude transmission coefficients.
Convenience wrapper with scalar support. NOT called from JIT boundaries. JIT'd coherent kernels call _fresnel_core or use the fresnel_operator module.
Source code in src/aegis/tissue/fresnel.py
xi_from_mu ¶
Normal wave-vector component in tissue. Uses xp, JIT-safe.
T0 ¶
Normal-incidence power transmission.
Source code in src/aegis/tissue/fresnel.py
Cole-Cole model¶
aegis.tissue.cole_cole ¶
4-pole Cole-Cole model for complex permittivity of biological tissues.
Implements the Gabriel (1996) parametric model used in the IT'IS v5.0 database. Each tissue is described by 14 parameters: ef (high-frequency permittivity), 4 x (delta, tau, alpha) dispersion poles, and static conductivity sig.
cole_cole_permittivity ¶
Complex permittivity from the 4-pole Cole-Cole model.
Parameters¶
freq_hz Frequency in Hz. Scalar or array. params Gabriel model parameters with keys: ef, del1..del4, tau1..tau4, alf1..alf4, sig.
Returns¶
Complex permittivity (eps_r - j*sigma/(omega*eps_0) combined).
Source code in src/aegis/tissue/cole_cole.py
debye_permittivity ¶
debye_permittivity(freq_hz: float | ndarray, eps_inf: float, eps_static: float, sigma: float, tau_s: float) -> complex | np.ndarray
Single-pole Debye permittivity model.
Parameters¶
freq_hz : frequency in Hz (scalar or array) eps_inf : high-frequency permittivity limit eps_static : static (DC) permittivity sigma : static conductivity in S/m tau_s : relaxation time in seconds
Source code in src/aegis/tissue/cole_cole.py
IT'IS database¶
aegis.tissue.database ¶
IT'IS v5.0 tissue database loader.
Reads Gabriel model parameters from the SQLite database and computes frequency-dependent tissue properties via the 4-pole Cole-Cole model.
find_database ¶
Locate the IT'IS v5.0 SQLite database.
Search order: 1. AEGIS_DATA_DIR environment variable 2. Repository data/ directory (aegis/data/)
Source code in src/aegis/tissue/database.py
get_gabriel_params ¶
Extract Gabriel model parameters (14 doubles) from the database.
Parameters¶
tissue_name Tissue name as stored in the database (e.g. "Skin", "Muscle"). db_path Explicit path to itis_v5.db. Auto-detected if None.
Returns¶
Dict with keys: ef, del1..del4, tau1..tau4, alf1..alf4, sig. None if tissue not found.
Source code in src/aegis/tissue/database.py
get_tissue_properties ¶
Full tissue EM properties at a given frequency from the IT'IS database.
Parameters¶
tissue_name Tissue name (e.g. "Skin"). freq_hz Frequency in Hz. db_path Explicit path to itis_v5.db. Auto-detected if None.
Returns¶
Dict with keys: freq_hz, eps_r, sigma, m (complex refractive index), n, kappa, abs_m, T0.
Source code in src/aegis/tissue/database.py
get_tissue_spectrum ¶
Vectorized tissue properties across a frequency array.
Single database lookup, then vectorized Cole-Cole evaluation. Much faster than calling get_tissue_properties in a loop.
Parameters¶
tissue_name Tissue name (e.g. "Skin"). freqs_hz 1D array of frequencies in Hz. db_path Explicit path to itis_v5.db. Auto-detected if None.
Returns¶
Dict with keys: freqs_hz, eps_r, sigma, n, kappa, T0 (all 1D arrays).
Source code in src/aegis/tissue/database.py
Geometry¶
BodyMesh¶
aegis.geometry.mesh.BodyMesh dataclass ¶
Triangulated body surface mesh.
All arrays are read-only views after construction.
Attributes¶
vertices : (N, 3, 3) triangle vertices normals : (N, 3) unit outward normals centroids : (N, 3) triangle centroids areas : (N,) triangle areas in mesh units squared
Source code in src/aegis/geometry/mesh.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 | |
vertex_hash property ¶
Pose-dependent content hash of the raw vertices.
Distinct from geometry_hash (rigid-invariant). Visibility is direction-dependent, so a yawed body must miss the cache. Quantized to float32 so float64 round-off does not spuriously change the key.
bounding_box property ¶
Return (bmin, bmax) of the mesh, cached after first access.
from_arrays classmethod ¶
Create a BodyMesh from raw vertex arrays.
Centroids and areas are computed automatically. If normals are not provided, they are computed from the vertex cross product.
Parameters¶
vertices : (N, 3, 3) triangle vertices normals : (N, 3) unit outward normals, or None to compute from vertices name : mesh name
Source code in src/aegis/geometry/mesh.py
sphere classmethod ¶
Create a sphere mesh via icosphere subdivision.
Parameters¶
radius : float Sphere radius. Must be positive. n_subdivisions : int Number of subdivision iterations. 0 gives a bare icosahedron (20 triangles). Each iteration multiplies the triangle count by 4.
Source code in src/aegis/geometry/mesh.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | |
cylinder classmethod ¶
Create a capped cylinder mesh aligned along the z-axis.
The cylinder spans from z = -height/2 to z = +height/2.
Parameters¶
radius : float Cylinder radius. Must be positive. height : float Cylinder height. Must be positive. n_segments : int Number of azimuthal divisions. Must be >= 3.
Source code in src/aegis/geometry/mesh.py
load staticmethod ¶
Load a binary STL file and return a BodyMesh.
Source code in src/aegis/geometry/mesh.py
save_binary_stl ¶
Write a binary STL (little-endian float32) for this mesh.
Uses vectorized numpy writes for speed on large meshes.
Source code in src/aegis/geometry/mesh.py
Mesh utilities¶
aegis.geometry.mesh.load_stl_binary ¶
Load a binary STL file.
Uses vectorized numpy reads instead of per-triangle struct.unpack, giving ~50-100x speedup on large meshes (100k+ triangles).
Returns¶
vertices : (N, 3, 3) Triangle vertices. normals : (N, 3) Unit triangle normals. centroids : (N, 3) Triangle centroids.
Source code in src/aegis/geometry/mesh.py
aegis.geometry.mesh.triangle_areas ¶
Compute area of each triangle from a (N, 3, 3) vertex array.
Source code in src/aegis/geometry/mesh.py
Projected area¶
aegis.geometry.projected_area ¶
Projected area A_perp(k_hat) lookup table.
Extracted from scripts/compute_projected_area_table.py.
fibonacci_sphere ¶
Deterministic near-uniform sampling on S^2 via golden spiral.
Returns k_hat of shape (n, 3).
Source code in src/aegis/geometry/projected_area.py
compute_projected_area ¶
compute_projected_area(normals: ndarray, areas: ndarray, k_hat: ndarray, chunk_dirs: int = 128) -> np.ndarray
Compute A_perp(k_hat) = sum_j a_j * [n_j . (-k_hat)]_+ for each direction.
Parameters¶
normals : (M, 3) unit triangle normals areas : (M,) triangle areas k_hat : (N, 3) incident directions (unit vectors) chunk_dirs : chunk size for memory-bounded BLAS
Returns¶
A_perp : (N,) projected areas
Source code in src/aegis/geometry/projected_area.py
Directivity and spherical harmonics¶
aegis.geometry.directivity ¶
Body absorption directivity D(k_hat) and spherical harmonic compression.
Extracted from scripts/compute_body_directivity.py.
spherical_angles_from_k_hat ¶
Convert unit vectors to (theta, phi).
theta: polar angle in [0, pi], measured from +z. phi: azimuth in [-pi, pi], measured from +x toward +y.
Source code in src/aegis/geometry/directivity.py
compute_directivity ¶
Compute directivity D = A_perp / mean(A_perp).
D has mean 1 by construction.
Source code in src/aegis/geometry/directivity.py
fit_sh ¶
Fit complex SH coefficients via least squares.
Parameters¶
D : (N,) directivity samples theta, phi : (N,) spherical angles L : maximum SH degree
Returns¶
c : ((L+1)^2,) complex coefficients
Source code in src/aegis/geometry/directivity.py
eval_sh ¶
Evaluate SH expansion at given angles.
Returns real-valued reconstruction.
Source code in src/aegis/geometry/directivity.py
sh_reconstruction_error ¶
Fit SH at degree L and return error metrics.
Returns dict with keys: L, n_coeff, rms, max_abs, p99_abs, coefficients.
Source code in src/aegis/geometry/directivity.py
Ambient occlusion¶
aegis.geometry.occlusion ¶
Cosine-weighted ambient occlusion (exposure fraction eta).
Extracted from scripts/compute_exposure_fraction_eta.py. Uses Numba JIT compilation for the BVH traversal and ray intersection hot path when available, giving ~50-100x speedup on large meshes.
cosine_weighted_hemisphere_samples ¶
Sample n directions on the +Z hemisphere with cosine-weighted distribution.
Returns (n, 3) array with z >= 0.
Source code in src/aegis/geometry/occlusion.py
make_tangent_frame ¶
Build orthonormal (t, b) for unit normal n so that t x b = n.
Source code in src/aegis/geometry/occlusion.py
build_bvh ¶
build_bvh(tri_bmin: ndarray, tri_bmax: ndarray, tri_centroids: ndarray, max_leaf: int = 8) -> tuple[dict[str, np.ndarray], np.ndarray]
Build a median-split BVH over triangle AABBs.
Returns (bvh_dict, tri_indices) where bvh_dict has keys bmin, bmax, left, right, start, count.
Source code in src/aegis/geometry/occlusion.py
ray_mesh_any_hit ¶
ray_mesh_any_hit(ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, bvh: dict[str, ndarray], tri_indices: ndarray, tri_v0x: ndarray, tri_v0y: ndarray, tri_v0z: ndarray, tri_e1x: ndarray, tri_e1y: ndarray, tri_e1z: ndarray, tri_e2x: ndarray, tri_e2y: ndarray, tri_e2z: ndarray, ignore_tri: int | None, t_min: float) -> bool
BVH traversal for any-hit ray-mesh intersection.
Public API wrapper that extracts flat arrays from the BVH dict and delegates to the Numba-JIT inner function.
Source code in src/aegis/geometry/occlusion.py
batch_closest_hits ¶
batch_closest_hits(origins: ndarray, dirs: ndarray, ignore: ndarray, bvh: dict[str, ndarray], tri_order: ndarray, tri_data: dict[str, ndarray], t_min: float) -> np.ndarray
Nearest hit triangle index for each ray, -1 on miss.
Parameters¶
origins : (R, 3) ray origins dirs : (R, 3) ray directions (need not be normalised) ignore : (R,) triangle index to skip per ray (e.g. the originating triangle) bvh, tri_order, tri_data : structures from build_bvh / _precompute_triangle_data t_min : minimum ray parameter to count as a hit (self-intersection guard)
Returns¶
(R,) int array of hit triangle indices, -1 where the ray escapes.
Source code in src/aegis/geometry/occlusion.py
compute_ambient_occlusion ¶
compute_ambient_occlusion(mesh: BodyMesh, n_rays: int = 64, seed: int = 0, max_leaf: int = 8) -> np.ndarray
Compute cosine-weighted ambient occlusion (exposure fraction eta).
For each triangle, eta is the fraction of cosine-weighted hemisphere directions that are not occluded by other triangles.
Parameters¶
mesh : BodyMesh n_rays : number of hemisphere samples per triangle seed : RNG seed for reproducibility max_leaf : BVH leaf size
Returns¶
eta : (N,) array in [0, 1]
Source code in src/aegis/geometry/occlusion.py
807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 | |
Spatial averaging¶
aegis.geometry.averaging ¶
ICNIRP 4 cm^2 spatial averaging for absorbed power density.
Extracted from scripts/sab_demo.py.
apply_spatial_averaging ¶
apply_spatial_averaging(sab: ndarray, centroids: ndarray, areas: ndarray, target_area_m2: float = 0.0004) -> np.ndarray
Apply ICNIRP 4 cm^2 spatial averaging to per-triangle S_ab.
For each triangle, find the minimal set of neighbours (sorted by distance) whose cumulative area reaches target_area. The averaged S_ab is the area-weighted mean over that patch.
Parameters¶
sab : (M,) per-triangle absorbed power density centroids : (M, 3) triangle centroids areas : (M,) triangle areas target_area_m2 : target averaging area (default 4e-4 = 4 cm^2)
Returns¶
sab_avg : (M,) spatially averaged S_ab
Source code in src/aegis/geometry/averaging.py
precompute_averaging_matrix ¶
precompute_averaging_matrix(centroids: ndarray, areas: ndarray, target_area_m2: float = 0.0004) -> sparse.csr_array
Build a sparse area-weighted averaging matrix G.
G is row-stochastic: each row sums to 1, so G @ sab gives the spatially averaged absorbed power density. Precomputing G lets us reuse the same geometry for many fields and, when converted to a dense JAX array, enables automatic differentiation through the averaging step.
Uses Numba JIT compilation when available for ~15-30x speedup over the pure-Python loop. Falls back to NumPy otherwise.
The query radius is sized just above the largest patch that fills target_area (see _RADIUS_MARGIN). Rows whose ball under-fills are re-queried over the full mesh, so the matrix is independent of the ball radius and identical to a brute-force nearest-neighbour accumulation.
Parameters¶
centroids : (M, 3) triangle centroids areas : (M,) triangle areas in m^2 target_area_m2 : target averaging area (default 4e-4 = 4 cm^2)
Returns¶
G : (M, M) sparse CSR matrix, row-stochastic
Source code in src/aegis/geometry/averaging.py
averaging_matrix_to_jax ¶
Convert a scipy sparse averaging matrix to a JAX BCOO sparse array.
Uses JAX experimental sparse BCOO format instead of materializing a dense (M, M) matrix, which would OOM for meshes with >10k triangles.
Parameters¶
G : sparse averaging matrix from precompute_averaging_matrix
Returns¶
jax.experimental.sparse.BCOO : sparse JAX array supporting G_jax @ sab and automatic differentiation through the averaging step.
Source code in src/aegis/geometry/averaging.py
Cauchy formula¶
aegis.geometry.cauchy ¶
Cauchy surface area formula: A_ab = A_total / 4 for convex bodies.
For non-convex bodies, A_ab = mean(A_perp) which equals A_total/4 only in the convex case.
cauchy_projected_area ¶
mean_projected_area ¶
cauchy_relative_error ¶
Relative deviation of mean(A_perp) from the Cauchy value A_total/4.
Positive means the body "exposes more" than a convex body of the same surface area (self-occlusion reduces this for non-convex bodies).
Source code in src/aegis/geometry/cauchy.py
Kernels¶
Level 0: worst-case bound¶
aegis.kernels.level0_bound ¶
Level 0: Worst-case power bound.
level0_bound ¶
level0_bound(total_area: float, A_ab: float, D_max: float, power: NDArray[floating], T0: float, n_triangles: int) -> tuple[NDArray[np.floating], np.floating]
Compute worst-case absorbed power bound.
Source code in src/aegis/kernels/level0_bound.py
Level 1: aggregate¶
aegis.kernels.level1_aggregate ¶
Level 1: Aggregate absorbed power via directivity.
level1_aggregate ¶
level1_aggregate(total_area: float, A_ab: float, k_hat: NDArray[floating], power: NDArray[floating], T0: float, n_triangles: int, sh_coeffs: NDArray[number] | None = None, sh_L: int = 4, D_table: NDArray[floating] | None = None, D_dirs: NDArray[floating] | None = None) -> tuple[NDArray[np.floating], float]
Compute aggregate absorbed power via directivity.
Source code in src/aegis/kernels/level1_aggregate.py
Level 2: geometric¶
aegis.kernels.level2_geometric ¶
Level 2: Geometric ReLU spatial map.
level2_geometric ¶
level2_geometric(normals: NDArray[floating], k_hat: NDArray[floating], power: NDArray[floating], T0: float) -> NDArray[np.floating]
Compute per-triangle S_ab using the geometric absorption law.
Parameters¶
normals : (M, 3) unit outward normals k_hat : (N, 3) incident directions (unit vectors) power : (N,) per-path power density [W/m^2] T0 : normal-incidence transmission coefficient
Returns¶
sab : (M,) absorbed power density per triangle [W/m^2]
Source code in src/aegis/kernels/level2_geometric.py
Level 3: Fresnel¶
aegis.kernels.level3_fresnel ¶
Level 3: Exact Fresnel spatial map.
level3_fresnel ¶
level3_fresnel(normals: NDArray[floating], k_hat: NDArray[floating], power: NDArray[floating], n_tilde: complex | NDArray[complexfloating]) -> NDArray[np.floating]
Compute per-triangle S_ab with angle-dependent Fresnel transmission.
Source code in src/aegis/kernels/level3_fresnel.py
Level 4: polarisation¶
aegis.kernels.level4_polarisation ¶
Level 4: Polarisation-aware Fresnel map.
level4_polarisation ¶
level4_polarisation(normals: NDArray[floating], k_hat: NDArray[floating], power: NDArray[floating], n_tilde: complex | NDArray[complexfloating], q: float | NDArray[floating] = 0.0, psi: NDArray[complexfloating] | None = None) -> NDArray[np.floating]
Compute per-triangle S_ab with polarisation correction.
When psi is given the physical per-(triangle, path) TE/TM split is used (T_eff = w_s*T_s + w_p*T_p); otherwise the legacy scalar/array q knob applies (T_eff = T_avg + (q/2) DeltaT).
Source code in src/aegis/kernels/level4_polarisation.py
Level 5: curvature¶
aegis.kernels.level5_curvature ¶
Level 5: Curvature correction.
level5_curvature ¶
level5_curvature(normals: NDArray[floating], k_hat: NDArray[floating], power: NDArray[floating], n_tilde: complex | NDArray[complexfloating], T0: float, curvature_H: NDArray[floating], freq_hz: float) -> NDArray[np.floating]
Compute per-triangle S_ab with Fresnel + curvature correction.
Source code in src/aegis/kernels/level5_curvature.py
Level 6: diffraction¶
aegis.kernels.level6_diffraction ¶
Level 6: Diffraction smoothing (ReLU -> physical GELU -> Fock).
level6_diffraction ¶
level6_diffraction(normals: NDArray[floating], k_hat: NDArray[floating], power: NDArray[floating], n_tilde: complex | NDArray[complexfloating], T0: float, curvature_H: NDArray[floating], freq_hz: float, *, diffraction_model: str | None = None, fock_R: NDArray[floating] | None = None, q_F_s: complex | None = None, q_F_h: complex | None = None, clearance: NDArray[floating] | None = None, R_occ: NDArray[floating] | None = None, distal_d1: NDArray[floating] | None = None, distal_d2: NDArray[floating] | None = None) -> NDArray[np.floating]
Compute per-triangle S_ab with Fresnel + curvature + diffraction.
The shadow-edge gate is selected by diffraction_model: "none" is exact ReLU, "gelu" is the legacy physical-GELU smoothing (the historical default), "fock" is the smooth-convex-body (Fock) gate (requires fock_R from geometry.curvature.fock_radius). When diffraction_model is None the gate defaults to "gelu" to preserve the pre-selector behaviour of this kernel.
Source code in src/aegis/kernels/level6_diffraction.py
Level 7: coherent¶
aegis.kernels.level7_coherent ¶
Level 7: coherent MIMO absorption map.
S_ab® = ||G_tilde® @ x||^2
Computes per-triangle absorbed power density from the body-surface channel G_tilde® and a precoding vector x. Uses Approximations 1 and 2 from the monograph (combined error < 5% for skin at 28 GHz).
Monograph: thm:coherent-law (Theorem 4.1).
level7_coherent ¶
level7_coherent(normals: NDArray[floating], centroids: NDArray[floating], areas: NDArray[floating], k_hat: NDArray[floating], psi: NDArray[complexfloating], element_index: NDArray[integer], x: NDArray[complexfloating], n_tilde: complex | NDArray[complexfloating], sigma: float, freq_hz: float, n_elements: int, h: NDArray[complexfloating] | None = None, fock_R: NDArray[floating] | None = None, q_F_s: complex | None = None, q_F_h: complex | None = None, clearance: NDArray[floating] | None = None, R_occ: NDArray[floating] | None = None, distal_d1: NDArray[floating] | None = None, distal_d2: NDArray[floating] | None = None) -> tuple[NDArray[np.floating], NDArray[np.complexfloating], NDArray[np.floating], float | None]
Compute coherent absorbed power density map.
Parameters¶
normals : (M, 3) centroids : (M, 3) areas : (M,) k_hat : (N, 3) psi : (N, 3) element_index : (N,) x : (M_ant,) precoding vector n_tilde : complex refractive index sigma : tissue conductivity [S/m] freq_hz : frequency [Hz] n_elements : int h : (M_ant,) UE channel vector (optional, for rho computation) fock_R : (M,) or (M, N) or None In-incidence-plane curvature radius [m] for the Fock shadow gate. None disables the gate (exact GO/Fresnel channel, back-compat). q_F_s, q_F_h : complex or None Soft/hard impedance-Fock parameters (None selects the PEC gate).
Returns¶
sab : (M,) absorbed power density per triangle [W/m^2] Q : (M_ant, M_ant) exposure operator eigenvalues : (M_ant,) or None, eigenvalues of Q rho : float or None, exposure-signal alignment
Source code in src/aegis/kernels/level7_coherent.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
Level 8: ECBF¶
aegis.kernels.level8_ecbf ¶
Level 8: exposure-constrained beamforming (ECBF).
Same as Level 7, but solves the QCQP to find the optimal precoder x* that maximizes signal power |h^T x|^2 subject to absorbed power and transmit power constraints.
Monograph: sec:ecbf, eq:QCQP, eq:optimal-x.
level8_ecbf ¶
level8_ecbf(normals: NDArray[floating], centroids: NDArray[floating], areas: NDArray[floating], k_hat: NDArray[floating], psi: NDArray[complexfloating], element_index: NDArray[integer], h: NDArray[complexfloating], n_tilde: complex | NDArray[complexfloating], sigma: float, freq_hz: float, n_elements: int, P: float = 1.0, P_abs_max: float = DEFAULT_P_ABS_MAX, fock_R: NDArray[floating] | None = None, q_F_s: complex | None = None, q_F_h: complex | None = None, clearance: NDArray[floating] | None = None, R_occ: NDArray[floating] | None = None, distal_d1: NDArray[floating] | None = None, distal_d2: NDArray[floating] | None = None) -> tuple[NDArray[np.floating], NDArray[np.complexfloating], NDArray[np.floating], NDArray[np.complexfloating], float]
Compute ECBF-optimised absorbed power density map.
Parameters¶
normals : (M, 3) centroids : (M, 3) areas : (M,) k_hat : (N, 3) psi : (N, 3) element_index : (N,) h : (M_ant,) UE channel vector n_tilde : complex refractive index sigma : tissue conductivity [S/m] freq_hz : frequency [Hz] n_elements : int P : total transmit power [W] P_abs_max : maximum absorbed power [W] fock_R : (M,) or (M, N) or None In-incidence-plane curvature radius [m] for the Fock shadow gate. None disables the gate (exact GO/Fresnel channel, back-compat). q_F_s, q_F_h : complex or None Soft/hard impedance-Fock parameters (None selects the PEC gate).
Returns¶
sab : (M,) absorbed power density per triangle [W/m^2] Q : (M_ant, M_ant) exposure operator eigenvalues : (M_ant,) eigenvalues of Q (descending) x_star : (M_ant,) optimal precoding vector rho : float, exposure-signal alignment
Source code in src/aegis/kernels/level8_ecbf.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |
Coherent module¶
Fresnel operator¶
aegis.coherent.fresnel_operator ¶
Fresnel transmission operator F_n® for coherent dosimetry.
Each path n at each surface point r has a rank-2 operator that projects the incident polarisation-amplitude vector onto TE/TM components and scales by the complex Fresnel transmission coefficients t_s, t_p.
F_n(r) = t_s * e_s @ e_s^T + t_p * e_p @ e_p^T (front-facing)
F_n(r) = 0 (back-facing)
Monograph: Definition in sec:fresnel-operator, Approximation 1 applied.
te_tm_basis ¶
te_tm_basis(k_hat: NDArray[floating], normals: NDArray[floating]) -> tuple[NDArray[np.floating], NDArray[np.floating]]
Compute TE and TM basis vectors for each (path, triangle) pair.
Parameters¶
k_hat : (N, 3) Unit directions of arrival. normals : (M, 3) Unit outward normals per triangle.
Returns¶
e_s : (M, N, 3) TE (s-polarisation) unit vectors. e_p : (M, N, 3) TM (p-polarisation) unit vectors.
Source code in src/aegis/coherent/fresnel_operator.py
fresnel_coeffs_from_mu ¶
TE/TM transmission amplitudes from a precomputed incidence cosine.
The frequency- and tissue-dependent half of :func:compute_fresnel_operator: given mu = n_hat . (-k_hat) (pure geometry, freq-invariant) and the complex refractive index n_tilde, returns the gated (t_s, t_p). Split out so a multi-frequency sweep over fixed geometry can reuse mu (and the TE/TM basis) and recompute only this part per frequency.
Returns (t_s, t_p) each shaped like mu, zero for back-facing paths.
Source code in src/aegis/coherent/fresnel_operator.py
compute_fresnel_operator ¶
Compute Fresnel operator components for each (triangle, path) pair.
Returns the ingredients needed to build F_n® * psi_n for each pair.
Parameters¶
normals : (M, 3) Unit outward normals. k_hat : (N, 3) Unit directions of arrival. n_tilde : complex Complex refractive index.
Returns¶
mu : (M, N) Incidence cosine n_hat . (-k_hat). Negative for back-facing. t_s : (M, N) Complex TE transmission amplitude. Zero where mu <= 0. t_p : (M, N) Complex TM transmission amplitude. Zero where mu <= 0. e_s : (M, N, 3) TE basis vectors. e_p : (M, N, 3) TM basis vectors.
Source code in src/aegis/coherent/fresnel_operator.py
apply_fresnel_operator ¶
Apply Fresnel operator: F_n® @ psi_n for each (triangle, path) pair.
F_n @ psi = t_s * (e_s . psi) * e_s + t_p * (e_p . psi) * e_p
Parameters¶
psi : (N, 3) Complex polarisation-amplitude vectors. t_s : (M, N) TE amplitude coefficients. t_p : (M, N) TM amplitude coefficients. e_s : (M, N, 3) TE basis vectors. e_p : (M, N, 3) TM basis vectors.
Returns¶
F_psi : (M, N, 3) Fresnel-transmitted field component per (triangle, path).
Source code in src/aegis/coherent/fresnel_operator.py
Field channel¶
aegis.coherent.field_channel ¶
Field channel matrix G® construction from propagation paths.
G® = [g_1®, ..., g_M®] in C^{3 x M_ant} where g_j® = sum_{n: j(n)=j} psi_n * exp(-i*k0 * k_hat_n . r)
The total electric field at surface point r is E® = G® @ x.
Monograph: Definition in sec:field-channel, eq:G-def.
compute_field_channel ¶
Build the field channel matrix G® at each triangle centroid.
Parameters¶
centroids : (M, 3) Triangle centroid positions [m]. k_hat : (N, 3) Unit directions of arrival. psi : (N, 3) Complex polarisation-amplitude vectors. element_index : (N,) Antenna element index j(n) for each path. freq_hz : float Frequency [Hz]. n_elements : int Total number of antenna elements M_ant.
Returns¶
G : (M_tri, 3, M_ant) Field channel matrix at each triangle centroid.
Source code in src/aegis/coherent/field_channel.py
Body channel¶
aegis.coherent.body_channel ¶
Body-surface channel G_tilde® with Fresnel filtering and depth coupling.
G_tilde® = [g_tilde_1®, ..., g_tilde_M®] in C^{3 x M_ant} where g_tilde_j® = sum_{n: j(n)=j} sqrt(sigma/(4*alpha_n)) * F_n® @ psi_n * exp(-i*k0*k_hat_n.r)
S_ab® = ||G_tilde® @ x||^2 (Theorem 4.1, coherent absorption law)
Uses Approximation 2: depth coupling factors Gamma_{nn'} ~ 1 (error < 0.44% for skin at 28 GHz), which allows the double sum to factor into a squared norm.
Monograph: eq:Gtilde-def, thm:coherent-law.
BodyChannelGeometry dataclass ¶
Frequency-invariant geometry for the simple (ungated) body channel.
Holds the parts of G_tilde that depend only on geometry, not on frequency or tissue: the incidence cosine mu, the TE/TM basis vectors, and the geometric phase dot centroids @ k_hat.T. Built once per (body chunk, ray set) and reused across a frequency sweep by :func:body_channel_from_geometry.
Source code in src/aegis/coherent/body_channel.py
compute_body_channel ¶
compute_body_channel(normals: ndarray, centroids: ndarray, k_hat: ndarray, psi: ndarray, element_index: ndarray, n_tilde: complex | ndarray, sigma: float, freq_hz: float, n_elements: int, fock_R: ndarray | None = None, q_F_s: complex | None = None, q_F_h: complex | None = None, clearance: ndarray | None = None, R_occ: ndarray | None = None, distal_d1: ndarray | None = None, distal_d2: ndarray | None = None) -> np.ndarray
Build the body-surface channel G_tilde® at each triangle centroid.
Parameters¶
normals : (M, 3) Unit outward normals. centroids : (M, 3) Triangle centroid positions [m]. k_hat : (N, 3) Unit directions of arrival. psi : (N, 3) Complex polarisation-amplitude vectors. element_index : (N,) Antenna element index j(n) for each path. n_tilde : complex Complex refractive index of tissue. sigma : float Tissue conductivity [S/m]. freq_hz : float Frequency [Hz]. n_elements : int Total number of antenna elements M_ant. fock_R : (M,) or (M, N) or None In-incidence-plane radius of curvature [m] for the Fock shadow gate. None (default) disables the gate, reproducing the ungated channel bit-for-bit (back-compat). q_F_s, q_F_h : complex or None Impedance-Fock parameters for the soft (TE) and hard (TM) creeping constants. None selects the PEC Fock gate.
Returns¶
G_tilde : (M_tri, 3, M_ant) Body-surface channel matrix at each triangle centroid.
Source code in src/aegis/coherent/body_channel.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | |
precompute_body_channel_geometry ¶
precompute_body_channel_geometry(normals, centroids, k_hat, psi, element_index, n_elements) -> BodyChannelGeometry
Build the frequency-invariant geometry for the simple body channel.
Pairs with :func:body_channel_from_geometry. For a single frequency the two together reproduce :func:compute_body_channel (ungated: no Fock gate, no distal shadow) bit-for-bit. The payoff is a frequency sweep over fixed geometry: mu, the TE/TM basis, and the geometric phase are computed once and reused for every frequency, so only the cheaper Fresnel / depth / phase assembly re-runs per frequency. On the studio 6-frequency grid that removes the dominant Fresnel-basis cost from five of every six builds.
Source code in src/aegis/coherent/body_channel.py
body_channel_from_geometry ¶
Assemble G_tilde for one frequency from precomputed geometry.
The frequency- and tissue-dependent half of the simple (ungated) body channel: Fresnel transmission, depth coupling, and the plane-wave phase, reusing the cached mu / TE-TM basis / geometric phase from :func:precompute_body_channel_geometry. The operations match :func:compute_body_channel exactly, so the NumPy result is bit-identical; under JAX the fused kernel (:func:_assemble_body_channel_jax) matches to FP32 reduction-order noise.
Source code in src/aegis/coherent/body_channel.py
compute_body_channel_factored ¶
compute_body_channel_factored(normals, centroids, center_k_hat, center_psi, element_psi, element_index, n_tilde, sigma, freq_hz, n_elements, fock_R=None, q_F_s=None, q_F_h=None, clearance=None, R_occ=None, distal_d1=None, distal_d2=None)
Build G_tilde using factored Fresnel for array-expanded paths.
When paths are expanded from N_center center paths to N_center*M_elements per-element paths (via expand_paths_to_array), the k_hat directions repeat across elements. This function computes the expensive Fresnel operator and depth coupling only for the N_center unique directions, then applies the per-element psi vectors. For a 4x4 UPA this is 16x less Fresnel work.
Parameters¶
normals : (M, 3) centroids : (M, 3) center_k_hat : (N_center, 3) Unique propagation directions (before array expansion). center_psi : (N_center, 3) Center-path psi (before element gain/phase, used only for shape). element_psi : (N_total, 3) Per-element psi vectors from expand_paths_to_array. element_index : (N_total,) Element index for each expanded path. n_tilde, sigma, freq_hz, n_elements : same as compute_body_channel. fock_R, q_F_s, q_F_h : same as compute_body_channel. fock_R is keyed on the center directions, so it is (M,) or (M, N_center). None disables the gate (back-compat).
Returns¶
G_tilde : (M, 3, n_elements) complex
Source code in src/aegis/coherent/body_channel.py
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 | |
Exposure operator¶
aegis.coherent.exposure_operator ¶
Exposure operator Q and its eigendecomposition.
Q = integral_Sigma G_tilde®^H @ G_tilde® dA in C^{M_ant x M_ant}
Total absorbed power: P_abs = x^H @ Q @ x Q is Hermitian positive-semidefinite by construction.
Monograph: def:Q, sec:exposure-operator.
compute_exposure_operator ¶
Compute the exposure operator Q from the body-surface channel.
Q = sum_m G_tilde[m]^H @ G_tilde[m] * area[m]
Parameters¶
G_tilde : (M_tri, 3, M_ant) Body-surface channel at each triangle centroid. areas : (M_tri,) Triangle areas [m^2].
Returns¶
Q : (M_ant, M_ant) Hermitian PSD exposure operator.
Source code in src/aegis/coherent/exposure_operator.py
eigendecompose_Q ¶
Eigendecompose the exposure operator Q.
Returns eigenvalues in descending order with corresponding eigenvectors.
Parameters¶
Q : (M_ant, M_ant) Hermitian PSD exposure operator.
Returns¶
eigenvalues : (M_ant,) Eigenvalues in descending order (non-negative). eigenvectors : (M_ant, M_ant) Columns are eigenvectors, sorted to match eigenvalues.
Source code in src/aegis/coherent/exposure_operator.py
compute_rho ¶
Compute exposure-signal alignment rho.
rho = h^T @ Q @ h* / (||h||^2 * lambda_max(Q))
This is the fraction of worst-case absorption achieved by MRT, where x_MRT = sqrt(P) * h* / ||h||. Monograph: eq:rho-def.
Parameters¶
h : (M_ant,) UE channel vector (complex). Q : (M_ant, M_ant) Exposure operator. lambda_max : float or None Largest eigenvalue of Q. Computed if not provided.
Returns¶
rho : float Alignment metric in [0, 1].
Source code in src/aegis/coherent/exposure_operator.py
ECBF solver¶
aegis.coherent.ecbf ¶
Exposure-constrained beamforming (ECBF) QCQP solver.
Solves
max_x |h^T x|^2 s.t. x^H Q x <= P_abs_max ||x||^2 <= P
Optimal solution
x* = sqrt(P) * (lambda*Q + nu*I)^{-1} h* / ||(lambda*Q + nu*I)^{-1} h*||
where lambda, nu >= 0 are Lagrange multipliers found by bisection on the complementary slackness conditions.
Monograph: sec:ecbf, eq:QCQP, eq:optimal-x.
solve_ecbf ¶
Solve the ECBF QCQP for the optimal precoding vector.
Parameters¶
h : (M_ant,) UE channel vector (complex). Q : (M_ant, M_ant) Exposure operator (Hermitian PSD). P_abs_max : float Maximum allowed absorbed power [W]. P : float Total transmit power budget [W]. tol : float Solver tolerance.
Returns¶
x_star : (M_ant,) Optimal precoding vector with ||x*||^2 <= P. The power constraint is active (||x*||^2 = P) at the QCQP optimum unless the parametric family cannot reach P_abs_max with full power, in which case the QCQP optimum lies in the power-slack regime with ||x*||^2 < P.
Source code in src/aegis/coherent/ecbf.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | |
solve_ecbf_sweep ¶
Solve the ECBF QCQP for many absorbed-power budgets sharing one (h, Q).
Equivalent to [solve_ecbf(h, Q, b, P) for b in p_abs_max_list], but the budget-independent work (the MRT reference and the eigendecomposition of Q) is done once and reused across every budget, so a budget sweep costs a single eigh instead of one per point. The exposure operator and the channel are fixed along an absorbed-power sweep, so only the scalar budget changes between points. Returns a list of precoders aligned with p_abs_max_list.
Source code in src/aegis/coherent/ecbf.py
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | |
Optimization¶
Loss functions¶
aegis.optim ¶
Differentiable optimization helpers for exposure-aware design.
Functions return raw arrays (JAX when available) for use inside jax.grad boundaries. All functions work with or without JAX installed.
peak_exposure ¶
total_absorbed_power ¶
soft_peak_exposure ¶
Smooth approximation to peak S_ab via log-sum-exp.
Higher temperature gives a tighter bound but sharper gradients. temperature=100 gives < 0.1 W/m^2 error for typical inputs.
Source code in src/aegis/optim/__init__.py
coherent_sab ¶
Per-triangle S_ab from body channel and precoding vector.
S_ab® = ||G_tilde® @ x||^2
This is the inner loop of beamforming optimization. Build G_tilde once with compute_body_channel(), then call this repeatedly while optimizing x.
Parameters¶
G_tilde : (M, 3, M_ant) body-surface channel x : (M_ant,) complex precoding vector
Returns¶
sab : (M,) absorbed power density [W/m^2]
Source code in src/aegis/optim/__init__.py
Constants¶
aegis.constants ¶
Physical constants used across AEGIS.
Compliance¶
aegis.compliance ¶
ICNIRP 2020 compliance limits for EMF exposure (100 kHz to 300 GHz).
Reference: ICNIRP, "Guidelines for Limiting Exposure to Electromagnetic Fields (100 kHz to 300 GHz)," Health Physics, vol. 118, no. 5, 2020.
Covers both general public and occupational scenarios. Above 6 GHz, absorbed power density (S_ab), incident power density (S_inc), and whole-body SAR limits are evaluated. Below 6 GHz, only whole-body SAR is checked (local SAR over 10 g cubic mass is not yet implemented).
ExposureScenario ¶
ICNIRPLimits dataclass ¶
ICNIRP 2020 limits for a specific scenario and frequency.
Attributes¶
scenario : ExposureScenario freq_hz : float sab_4cm2 : absorbed power density limit over 4 cm^2 [W/m^2], None if <= 6 GHz sab_1cm2 : absorbed power density limit over 1 cm^2 [W/m^2], None if <= 30 GHz sar_wb : whole-body SAR [W/kg] sinc_local : local incident power density limit [W/m^2] (Table 6), None if <= 6 GHz sinc_whole_body : whole-body incident power density limit [W/m^2] (Table 5), None if <= 6 GHz
Source code in src/aegis/compliance/__init__.py
ComplianceCheck dataclass ¶
Result of checking a single quantity against its ICNIRP limit.
Attributes¶
value : measured or computed value limit : ICNIRP limit unit : physical unit string label : human-readable name for this check
Source code in src/aegis/compliance/__init__.py
margin_db property ¶
Compliance margin in dB: 10 * log10(limit / value).
Positive means compliant, negative means exceeded.
ComplianceResult dataclass ¶
Full ICNIRP 2020 compliance assessment.
Attributes¶
scenario : ExposureScenario freq_hz : float sab_4cm2 : ComplianceCheck for S_ab over 4 cm^2 sab_1cm2 : ComplianceCheck for S_ab over 1 cm^2, or None if <= 30 GHz sar_wb : ComplianceCheck for whole-body SAR sinc_local : ComplianceCheck for local incident power density sinc_whole_body : ComplianceCheck for whole-body incident power density
Source code in src/aegis/compliance/__init__.py
icnirp_limits ¶
icnirp_limits(scenario: ExposureScenario = ExposureScenario.GENERAL_PUBLIC, freq_hz: float = DEFAULT_FREQ_HZ) -> ICNIRPLimits
Compute ICNIRP 2020 limits for a given scenario and frequency.
Parameters¶
scenario : ExposureScenario General public or occupational. freq_hz : float Frequency in Hz. Must be >= 100 kHz and <= 300 GHz. SAR_wb is returned across the whole range. Above 6 GHz the Sab and S_inc limits are also returned; below 6 GHz they are set to None because those basic restrictions do not apply.
Returns¶
ICNIRPLimits All applicable limits at this frequency.
Source code in src/aegis/compliance/__init__.py
evaluate_compliance ¶
evaluate_compliance(*, freq_hz: float, scenario: ExposureScenario = ExposureScenario.GENERAL_PUBLIC, sab_4cm2: float | None = None, sab_1cm2: float | None = None, sar_wb: float | None = None, sinc_local: float | None = None, sinc_whole_body: float | None = None) -> ComplianceResult
Evaluate ICNIRP 2020 compliance for measured/computed quantities.
Only checks applicable at freq_hz per ICNIRP 2020 are included:
- Whole-body SAR (
sar_wb) is a basic restriction across the entire 100 kHz - 300 GHz range (Table 2 and Table 5 both list it at 0.08 W/kg general public / 0.4 W/kg occupational), so it is always checked when provided. - Below or at 6 GHz (Table 2 regime): Sab (4 cm^2, 1 cm^2) and Sinc_local/whole_body limits do not apply and are omitted.
- Above 6 GHz (Table 5 regime): Sab and Sinc limits apply in addition to SAR_wb.
Parameters¶
freq_hz : float Frequency in Hz. Must be >= 100 kHz and <= 300 GHz. scenario : ExposureScenario General public or occupational. sab_4cm2 : float or None Measured S_ab averaged over 4 cm^2 [W/m^2]. sab_1cm2 : float or None Measured S_ab averaged over 1 cm^2 [W/m^2]. Ignored if freq <= 30 GHz. sar_wb : float or None Whole-body SAR [W/kg]. sinc_local : float or None Local incident power density [W/m^2]. sinc_whole_body : float or None Whole-body incident power density [W/m^2].
Returns¶
ComplianceResult
Source code in src/aegis/compliance/__init__.py
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | |
margin_db ¶
Compliance margin in dB: 10 * log10(limit / value).
Positive means compliant (value below limit), negative means exceeded. Raises ValueError if value <= 0.
Source code in src/aegis/compliance/__init__.py
max_compliant_power ¶
Compute the maximum transmit power that keeps all checks compliant.
For incoherent dosimetry (levels 0-6) and coherent (levels 7-8), S_ab scales linearly with transmit power P. Given a ComplianceResult computed at reference power ref_power_w, this function finds the largest P such that all measured quantities stay within their ICNIRP limits.
Parameters¶
result : ComplianceResult A compliance evaluation from evaluate_compliance(). ref_power_w : float The transmit power [W] at which the result was computed. Must be positive.
Returns¶
float Maximum compliant transmit power in watts. Returns inf if no checks are present or all measured values are zero. Returns 0.0 if any check has a zero limit (should not happen for valid ICNIRP).
Source code in src/aegis/compliance/__init__.py
summary_text ¶
Human-readable compliance summary.
Parameters¶
result : ComplianceResult The compliance evaluation result. tx_power_dbm : float or None Transmit power in dBm, shown if provided.
Returns¶
str Multi-line plain-text summary.
Source code in src/aegis/compliance/__init__.py
power_sweep ¶
power_sweep(result: ComplianceResult, ref_power_w: float, p_min_w: float | None = None, p_max_w: float | None = None, n_points: int = 200) -> dict[str, Any]
Compute compliance margin vs transmit power.
S_ab scales linearly with P, so all checks scale by P/P_ref. This function evaluates compliance across a power range without re-running the dosimetry engine.
Parameters¶
result : ComplianceResult A compliance evaluation at reference power ref_power_w. ref_power_w : float Transmit power [W] at which result was computed. p_min_w : float or None Minimum power [W]. Defaults to ref_power_w / 100. p_max_w : float or None Maximum power [W]. Defaults to ref_power_w * 100. n_points : int Number of power samples.
Returns¶
dict with keys: power_w : (n_points,) power in watts power_dbm : (n_points,) power in dBm margin_db : (n_points,) tightest compliance margin in dB compliant : (n_points,) boolean mask p_max_compliant_w : float, maximum compliant power [W]
Source code in src/aegis/compliance/__init__.py
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 | |
frequency_sweep ¶
frequency_sweep(*, sab_4cm2: float | None = None, sab_1cm2: float | None = None, sar_wb: float | None = None, sinc_local: float | None = None, sinc_whole_body: float | None = None, scenario: ExposureScenario = ExposureScenario.GENERAL_PUBLIC, freq_min_hz: float = 7000000000.0, freq_max_hz: float = 100000000000.0, n_points: int = 200) -> dict[str, Any]
Evaluate compliance across a frequency range.
The measured values are assumed constant (worst-case: same exposure level across frequency). The ICNIRP limits change with frequency (especially sinc_local ~ 1/f^0.177), so compliance margin varies.
This answers: "At which frequencies is this exposure level compliant?"
Parameters¶
sab_4cm2, sab_1cm2, sar_wb, sinc_local, sinc_whole_body : float or None Measured quantities (constant across frequency). scenario : ExposureScenario freq_min_hz, freq_max_hz : float Frequency range (must be within 100 kHz to 300 GHz). n_points : int Number of frequency samples.
Returns¶
dict with keys: freq_hz : (n_points,) frequency array freq_ghz : (n_points,) frequency in GHz margin_db : (n_points,) tightest margin at each frequency per_check_margin_db : dict[str, (n_points,) ndarray] Margin in dB for each individual check, by attribute name (sab_4cm2, sab_1cm2, sar_wb, sinc_local, sinc_whole_body). Points where the check does not apply at that frequency are NaN so the UI can break lines cleanly. compliant : (n_points,) boolean mask results : list of ComplianceResult at each frequency
Source code in src/aegis/compliance/__init__.py
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 | |
compliance_heatmap ¶
compliance_heatmap(*, sab_4cm2: float, ref_power_w: float = 1.0, scenario: ExposureScenario = ExposureScenario.GENERAL_PUBLIC, freq_min_hz: float = 7000000000.0, freq_max_hz: float = 100000000000.0, p_min_w: float | None = None, p_max_w: float | None = None, n_freq: int = 50, n_power: int = 50, sinc_local: float | None = None) -> dict[str, Any]
2D compliance map over frequency and transmit power.
Given an S_ab measurement at reference power, compute the compliance margin across the (frequency, power) plane. S_ab scales linearly with power. The ICNIRP S_ab limit is constant (20 or 100 W/m^2) but other limits (sinc_local) vary with frequency.
This answers: "For what (frequency, power) combinations is this exposure scenario compliant?"
Parameters¶
sab_4cm2 : float Peak spatially averaged S_ab [W/m^2] at ref_power_w. ref_power_w : float Transmit power [W] at which sab_4cm2 was measured. scenario : ExposureScenario freq_min_hz, freq_max_hz : float Frequency range. p_min_w, p_max_w : float or None Power range. Defaults to ref_power_w / 100 .. ref_power_w * 100. n_freq, n_power : int Grid resolution. sinc_local : float or None Peak incident power density S_inc [W/m^2] at ref_power_w. When provided, also checks the ICNIRP sinc_local limit (55/f_GHz^0.177 for GP, 275/f_GHz^0.177 for occupational) which varies with frequency, making the heatmap non-degenerate.
Returns¶
dict with keys: freq_hz : (n_freq,) frequency array power_w : (n_power,) power array power_dbm : (n_power,) power in dBm margin_db : (n_power, n_freq) margin heatmap (positive = compliant) compliant : (n_power, n_freq) boolean mask p_max_per_freq : (n_freq,) max compliant power at each frequency
Source code in src/aegis/compliance/__init__.py
682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 | |
link_budget_compliance ¶
link_budget_compliance(*, tx_power_w: float, antenna_gain_dbi: float = 0.0, distance_m: float, freq_hz: float, T0: float | None = None, scenario: ExposureScenario = ExposureScenario.GENERAL_PUBLIC) -> dict[str, Any]
Quick compliance check from RF link budget parameters.
Estimates incident power density from free-space path loss and computes approximate S_ab using normal-incidence transmission.
This is a conservative (worst-case) estimate: it assumes the body intercepts the full antenna beam at the given distance, with all power arriving at normal incidence. Real dosimetry with mesh geometry will give lower (more accurate) values.
Parameters¶
tx_power_w : float Transmit power [W]. Must be positive. antenna_gain_dbi : float Antenna gain [dBi]. Default 0 (isotropic). distance_m : float Distance from antenna to body [m]. Must be positive. freq_hz : float Frequency [Hz]. Must be in ICNIRP range (100 kHz to 300 GHz). T0 : float or None Normal-incidence transmission coefficient. If None, estimated from skin tissue at the given frequency. scenario : ExposureScenario General public or occupational.
Returns¶
dict with keys: sinc : float, incident power density [W/m^2] sab_estimate : float, estimated S_ab [W/m^2] T0 : float, transmission coefficient used compliance : ComplianceResult compliant : bool margin_db : float max_tx_power_w : float, max compliant TX power [W] max_tx_power_dbm : float, max compliant TX power [dBm]
Source code in src/aegis/compliance/__init__.py
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 | |
spatial_compliance_grid ¶
spatial_compliance_grid(*, station_lats: ndarray, station_lons: ndarray, station_eirp_dbm: ndarray, station_freq_hz: ndarray, station_heights_m: ndarray, grid_lats: ndarray, grid_lons: ndarray, scenario: ExposureScenario = ExposureScenario.GENERAL_PUBLIC, receiver_height_m: float = 1.5, T0: float | None = None) -> dict[str, Any]
Compute ICNIRP compliance margin at a grid of receiver locations.
For each grid point, sums incident power density from all stations using free-space path loss, estimates absorbed power density via T0, and evaluates the tightest ICNIRP compliance margin.
Uses per-station frequency for correct limit evaluation. Multi-frequency cumulative exposure is assessed using the ICNIRP summation rule: sum(value_i / limit_i) <= 1 across frequency groups.
Parameters¶
station_lats, station_lons : (N,) arrays Station positions in WGS84 degrees. station_eirp_dbm : (N,) array EIRP per station in dBm. station_freq_hz : (N,) array Operating frequency per station in Hz. station_heights_m : (N,) array Antenna height above ground per station in meters. grid_lats, grid_lons : (M,) arrays Receiver grid positions in WGS84 degrees. scenario : ExposureScenario receiver_height_m : float Receiver (body) height above ground in meters. T0 : float or None Normal-incidence transmission coefficient. If None, estimated from skin tissue at the EIRP-weighted mean frequency.
Returns¶
dict with keys: sinc : (M,) total incident power density at each grid point [W/m^2] sab_estimate : (M,) estimated S_ab at each grid point [W/m^2] margin_db : (M,) tightest ICNIRP compliance margin [dB] compliant : (M,) boolean, True if all limits satisfied freq_hz_dominant : float, EIRP-weighted mean frequency T0 : float, transmission coefficient used
Source code in src/aegis/compliance/__init__.py
917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 | |
is_compliant_sab ¶
Check if peak spatially averaged S_ab is below the ICNIRP limit.
Backward-compat function. Prefer evaluate_compliance() for new code.
Source code in src/aegis/compliance/__init__.py
is_compliant_sar ¶
Check if whole-body SAR is below the ICNIRP limit.
Backward-compat function. Prefer evaluate_compliance() for new code.
Source code in src/aegis/compliance/__init__.py
Analysis¶
Path contributions¶
aegis.analysis ¶
Path contribution analysis for dosimetry results.
Answers: which propagation paths contribute most to the peak exposure? This is essential for importance sampling in ray tracing, understanding exposure hotspots, and identifying dominant paths for mitigation.
path_contributions ¶
path_contributions(body: BodyMesh, paths: PropagationPaths, tissue: TissueModel, *, triangle_index: int | None = None, top_k: int | None = None) -> dict
Compute per-path contribution to absorbed power density.
For the geometric+Fresnel kernel (level 3), the contribution of path n to triangle m is: c_{m,n} = T_avg(mu_{m,n}) * ReLU(mu_{m,n}) * power_n. Total S_ab(m) = sum_n c_{m,n}.
Parameters¶
body : BodyMesh paths : PropagationPaths tissue : TissueModel triangle_index : int or None If given, compute contributions to this specific triangle. If None, compute contributions to the triangle with peak S_ab. top_k : int or None If given, return only the top-k contributing paths.
Returns¶
dict with keys: triangle_index : int, the target triangle sab_total : float, total S_ab at the target triangle path_indices : (K,) indices of contributing paths (sorted by contribution) contributions : (K,) per-path contribution to S_ab [W/m^2] fractions : (K,) fractional contribution (sums to 1) cumulative : (K,) cumulative fraction k_hat : (K, 3) directions of contributing paths power : (K,) incident power density of contributing paths
Source code in src/aegis/analysis.py
exposure_heatmap ¶
Compute full (M, N) contribution matrix.
Returns C where C[m, n] is the contribution of path n to triangle m's S_ab. S_ab = C @ ones gives per-triangle totals (but S_ab is already C @ power, so this matrix shows the spatial-angular coupling).
Parameters¶
body : BodyMesh paths : PropagationPaths tissue : TissueModel
Returns¶
C : (M, N) contribution matrix where C[m,n] = T_avg(mu) * ReLU(mu) * power_n
Source code in src/aegis/analysis.py
path_importance ¶
Compute per-path importance score for the overall exposure.
importance_n = sum_m area_m * C[m, n], i.e. the contribution of path n to total absorbed power P_abs. Paths with high importance are the ones to keep when pruning for faster computation.
Parameters¶
body : BodyMesh paths : PropagationPaths tissue : TissueModel
Returns¶
importance : (N,) per-path importance [W], sums to P_abs
Source code in src/aegis/analysis.py
Integration¶
DiffeRT bridge¶
aegis.integration.differt ¶
DiffeRT ray tracer integration.
Loads propagation paths from DiffeRT scene output and converts them to PropagationPaths for use with any AEGIS fidelity level.
Requires: pip install aegis[rt] (installs differt>=0.7.0)
paths_from_differt ¶
paths_from_differt(vertices: ndarray, normals: ndarray, path_vertices: ndarray, tx_positions: ndarray, freq_hz: float, tx_power_dbm: float = DEFAULT_POWER_DBM, element_indices: ndarray | None = None, object_indices: ndarray | None = None, material_indices: ndarray | None = None, material_n_tilde: list[complex] | None = None, initial_polarisation: str = 'vertical') -> PropagationPaths
Build PropagationPaths from DiffeRT ray tracing output.
This function takes the raw geometric output from DiffeRT's path solver and converts it to AEGIS PropagationPaths. It computes: - k_hat from the last path segment direction - psi from free-space path loss and Fresnel reflection at interactions - element_index from the transmitter array structure
When object_indices and material_n_tilde are provided, polarisation is tracked through each reflection using TE/TM decomposition and Fresnel coefficients. Otherwise, an arbitrary perpendicular is used (sufficient for incoherent levels 0-6 where only |psi|^2 matters).
Parameters¶
vertices : (N_scene, 3) scene triangle vertices (for material lookup) normals : (N_scene, 3) scene triangle normals path_vertices : (N_paths, N_bounces+2, 3) path vertex positions. First vertex is TX, last is the arrival point near the body. tx_positions : (M_ant, 3) transmitter antenna element positions freq_hz : operating frequency [Hz] tx_power_dbm : transmit power per element [dBm], default 43 (20 W) element_indices : (N_paths,) which TX element each path originates from. If None, inferred from nearest TX position. object_indices : (N_paths, path_length) triangle index at each path vertex from DiffeRT Paths.objects. -1 for TX/RX placeholders. material_indices : (N_triangles,) per-triangle material index from DiffeRT mesh.face_materials. material_n_tilde : list of complex refractive indices per material at the operating frequency. Use fresnel.n_complex() to compute. initial_polarisation : "vertical" or "horizontal" TX antenna polarisation.
Returns¶
PropagationPaths ready for any AEGIS level.
Source code in src/aegis/integration/differt.py
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 | |
paths_from_differt_scene ¶
paths_from_differt_scene(scene_path: str | Path, tx_positions: ndarray, rx_position: ndarray, freq_hz: float, max_bounces: int = 3, tx_power_dbm: float = DEFAULT_POWER_DBM, initial_polarisation: str = 'vertical') -> PropagationPaths
Run DiffeRT on a scene file and return PropagationPaths.
This is the high-level entry point. It loads a scene, runs ray tracing via scene.compute_paths(), and returns paths with proper TE/TM polarisation tracking through reflections.
Parameters¶
scene_path : path to Sionna/Mitsuba XML scene file tx_positions : (M_ant, 3) transmitter element positions [m] rx_position : (3,) receiver (body) position [m] freq_hz : operating frequency [Hz] max_bounces : maximum number of reflections (default 3) tx_power_dbm : transmit power per element [dBm] initial_polarisation : "vertical" or "horizontal" TX antenna polarisation
Returns¶
PropagationPaths
Source code in src/aegis/integration/differt.py
paths_from_differt_scene_obj ¶
paths_from_differt_scene_obj(scene, tx_positions: ndarray, rx_position: ndarray, freq_hz: float, max_bounces: int = 3, tx_power_dbm: float = DEFAULT_POWER_DBM, initial_polarisation: str = 'vertical') -> PropagationPaths
Ray trace an in-memory DiffeRT TriangleScene and return PropagationPaths.
Same as :func:paths_from_differt_scene but takes an already-loaded scene object instead of an XML path. Use this for AEGIS-native meshes via EnvironmentMesh.to_differt_scene(): the to_sionna_xml export targets Sionna's lenient Mitsuba loader and is not accepted by differt_core's stricter parser, and building the scene once avoids re-parsing per call.
Parameters¶
scene : differt.scene.TriangleScene with face_materials and material_names tx_positions : (M_ant, 3) transmitter element positions [m] rx_position : (3,) receiver (body) position [m] freq_hz : operating frequency [Hz] max_bounces : maximum number of reflections (default 3) tx_power_dbm : transmit power per element [dBm] initial_polarisation : "vertical" or "horizontal" TX antenna polarisation
Returns¶
PropagationPaths
Source code in src/aegis/integration/differt.py
Sionna RT bridge¶
aegis.integration.sionna ¶
Sionna RT ray tracer integration.
Converts Sionna RT channel coefficients to AEGIS PropagationPaths. Uses a dual-polarized isotropic RX probe to capture the full E-field polarisation state, then scales to absolute V/m using:
psi = sqrt(8*pi*Z_0*P_T) / lambda * (a_theta * e_theta + a_phi * e_phi)
Requires: pip install aegis[sionna] (installs sionna-rt>=1.0)
paths_from_sionna_scene ¶
paths_from_sionna_scene(scene, tx_positions: ndarray, rx_position: ndarray, freq_hz: float, max_bounces: int = 5, tx_power_dbm: float = DEFAULT_POWER_DBM, tx_pattern: str = 'isotropic', return_viz: bool = False, los: bool = True, specular_reflection: bool = True, diffuse_reflection: bool = False, refraction: bool = True, diffraction: bool = False, edge_diffraction: bool = False, diffraction_lit_region: bool = True, samples_per_src: int = 1000000, max_num_paths_per_src: int = 1000000, synthetic_array: bool = True, seed: int = DEFAULT_SEED, differentiable: bool = False) -> PropagationPaths | tuple[PropagationPaths, list[dict]]
Run Sionna RT and convert results to PropagationPaths.
Parameters¶
scene : sionna.rt Scene object (loaded externally) tx_positions : (M_ant, 3) transmitter element positions rx_position : (3,) body centroid position freq_hz : carrier frequency in Hz max_bounces : maximum number of ray interactions tx_power_dbm : transmit power per element [dBm] tx_pattern : TX antenna pattern name return_viz : if True, also return path visualization data differentiable : if True, return JAX arrays with Dr.Jit gradient tracking via paths.cir(out_type="jax"). Requires JAX. Invalid paths are masked to zero (static shapes) instead of filtered, enabling jax.grad through the conversion.
Returns¶
PropagationPaths with k_hat, psi, element_index, delay, is_los. If return_viz is True, returns (PropagationPaths, path_viz_list).
Source code in src/aegis/integration/sionna.py
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 | |