This commit is contained in:
Ján Malinovský 2026-10-21 14:14:51 +02:00
commit 6614ac837e
27 changed files with 1995 additions and 0 deletions

28
README.md Normal file
View File

@ -0,0 +1,28 @@
# MDS-MAP localization simulation
This project simulates localization in an ad-hoc/sensor network using MDS-MAP
and two anchor-only baselines: circular lateration (nonlinear least squares)
and square lateration (min-max bounds).
## Install and run
Use Python 3.10 or newer:
```powershell
python -m pip install -r requirements.txt
python scripts/run_single.py
python scripts/run_remote_carlo.py
pytest
```
The default experiment places 4 anchors and 16 unknown nodes in a 100 m
square, uses a 55 m radio range, Gaussian ranging noise, and 30 repetitions.
Outputs are written to `results/figures` and `results/tables`. Edit
`config/default.yaml` to change the experiment. A connected graph is required
for shortest-path completion; increase `radio_range` if a random run is
disconnected.
The position figure compares actual unknown positions with all estimates and
anchors. The error-distribution figure is a histogram over all unknown nodes
and repetitions. The metrics CSV contains RMSE, MAE, mean, standard deviation,
median, maximum, and 95th percentile errors for every repetition and method.

10
config/default.yaml Normal file
View File

@ -0,0 +1,10 @@
area_size: [100.0, 100.0]
anchor_count: 4
unknown_count: 16
radio_range: 55.0
noise_model: gaussian
noise_sigma: 1.0
multiplicative_sigma: 0.02
repetitions: 30
seed: 2026
output_dir: results

14
docs/report.md Normal file
View File

@ -0,0 +1,14 @@
# First-version report
The pipeline generates random node positions, measures only radio-connected
links, completes the distance matrix using all-pairs shortest paths, and
applies classical two-dimensional MDS. An SVD-based Procrustes similarity
transform maps the relative MDS coordinates to the known anchor coordinates.
Circular lateration estimates each unknown by minimizing range residuals.
Square lateration intersects axis-aligned square bounds around the anchors and
uses the center of the intersection. Both baselines use only direct
anchor-to-unknown measurements.
The Monte Carlo script records per-node errors and the metrics table, allowing
methods to be compared under the same generated networks and noise settings.

0
notebooks/analysis.ipynb Normal file
View File

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
numpy>=1.24
scipy>=1.10
matplotlib>=3.7
pandas>=2.0
networkx>=3.0
PyYAML>=6.0
pytest>=7.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,91 @@
repetition,method,rmse,mae,mean,std,median,max,p95
0,MDS-MAP,2.185218449708254,1.8458787821819298,1.8458787821819298,1.1695773571833123,1.662450878006072,3.9786433573742683,3.6190407320796423
0,Circular lateration,43.78947366843516,32.206482229766934,32.206482229766934,29.669184460350156,27.674471499205733,96.88504803126659,91.33955515642728
0,Square lateration,44.0939637244304,34.019038921879854,34.019038921879854,28.05321064981703,21.558708106491665,92.82196959355585,84.87322026587962
1,MDS-MAP,104.12076076763218,87.56542088437342,87.56542088437342,56.33320413550988,117.43184088620788,154.50445738294167,154.44482717944248
1,Circular lateration,60.89413219933657,54.2240019721968,54.2240019721968,27.71015962475634,58.87763429444088,113.07254488237886,107.54515548490463
1,Square lateration,61.569113134199235,58.60133441233858,58.60133441233858,18.884896007791138,59.719879091337816,86.93801531391847,84.01272360227419
2,MDS-MAP,8.309204011897831,7.295218265267347,7.295218265267347,3.9777709553779834,6.952625161531805,13.65028634375217,13.581143903853613
2,Circular lateration,58.90527147459289,49.75408151774144,49.75408151774144,31.533512012166153,41.24813251143627,94.71083181490397,92.94763204685577
2,Square lateration,51.428699145885744,49.24560206341943,49.24560206341943,14.825038726740953,47.562557048841356,82.0436751907646,76.29538685792052
3,MDS-MAP,4.562718714542838,3.6624244679488505,3.6624244679488505,2.7212219837067737,2.9239413324889405,10.496007008331187,8.816379048148963
3,Circular lateration,44.731144318040506,37.08045295423461,37.08045295423461,25.01829891719581,40.092685957413664,77.21968011045794,75.33668398773148
3,Square lateration,40.37648518063248,36.344284636138084,36.344284636138084,17.58844876130954,32.10106382140965,67.26928046498466,60.82472560027291
4,MDS-MAP,37.731531242875896,28.874548437590043,28.874548437590043,24.288452039135517,25.883270592181606,88.2057668995198,69.74594678025248
4,Circular lateration,37.232108800794904,30.564519359637014,30.564519359637014,21.261234255531907,32.34065231775183,63.88951680632521,61.58674330967082
4,Square lateration,34.70763712101917,31.378417118604823,31.378417118604823,14.832903075770295,31.399199886450468,55.700989968264395,48.73261798554818
5,MDS-MAP,8.745454088470565,4.7373231721726405,4.7373231721726405,7.351240465114897,3.037014129702911,32.773206869979084,12.253411191399938
5,Circular lateration,63.12743447788416,52.88505533350713,52.88505533350713,34.470913914943516,65.61575015282035,98.6286881422611,93.52117947675772
5,Square lateration,52.329087777359454,49.52081173409391,49.52081173409391,16.91220366501711,53.51933906158667,84.61054214239813,73.4930051835108
6,MDS-MAP,2.062801521780494,1.7897094052718303,1.7897094052718303,1.0257145621182693,1.6155498003539472,3.86694194821288,3.1457997444972765
6,Circular lateration,43.42985452935746,34.73688498965117,34.73688498965117,26.067241619643962,30.281996396910976,80.80282359292899,78.60966287898637
6,Square lateration,41.53781614889254,37.147005175669584,37.147005175669584,18.58736605595249,32.928524579174955,74.69818358853158,63.912291733742556
7,MDS-MAP,7.329853096812777,5.806914921001663,5.806914921001663,4.472860999528612,4.329566088507324,16.554267620021584,15.136518644770634
7,Circular lateration,49.22768079299547,38.04624224012751,38.04624224012751,31.238566030831056,40.33077934159579,96.17515806770614,92.41138053125128
7,Square lateration,46.23994292584898,39.98878001130518,39.98878001130518,23.217015204224907,41.27205620569523,90.19680481386244,73.89083724702131
8,MDS-MAP,2.360876038225514,2.1470962365985153,2.1470962365985153,0.9816890641399094,1.9499894362935801,5.277335426175595,3.668501609696447
8,Circular lateration,47.55326094328507,40.008212378826016,40.008212378826016,25.703220976969288,49.25537008761242,77.2192568520714,68.68686076482805
8,Square lateration,46.351806700557724,41.81042386154252,41.81042386154252,20.00945878638461,33.0027993825655,79.44514858379279,74.94772560022531
9,MDS-MAP,57.52895101157319,47.55908308254369,47.55908308254369,32.36840775879616,52.50280885079077,123.03355597499362,91.32771520237043
9,Circular lateration,45.87120443806874,37.9305514989483,37.9305514989483,25.796136524385343,36.23798412456007,103.85513225229437,74.77426123066019
9,Square lateration,37.80148521481428,33.79777306954802,33.79777306954802,16.931119868016896,30.317145927841928,63.88645630943104,57.18777726757146
10,MDS-MAP,1.85488763269496,1.685840525450664,1.685840525450664,0.7736600368850265,1.3525067450922261,3.2477428625751004,2.9707277070263696
10,Circular lateration,58.140560551166196,48.20074053107887,48.20074053107887,32.5117423934712,47.43354721443126,114.8385863544283,94.12169641919814
10,Square lateration,45.98335566739533,41.83650571762525,41.83650571762525,19.083390363698168,40.74436093552332,89.45446667558086,69.05719659815932
11,MDS-MAP,4.374690752867252,3.642163882505203,3.642163882505203,2.4233368391944325,3.302262558017393,11.475930347382185,6.885478285992145
11,Circular lateration,46.32486086309956,37.53540020502426,37.53540020502426,27.1493363718931,37.14343873879638,79.62054347862745,75.6832888293246
11,Square lateration,40.447821876509984,32.986906372453355,32.986906372453355,23.40705668231006,31.35540088032791,80.93635896211804,75.92612956207503
12,MDS-MAP,53.8776564431605,45.4481932648552,45.4481932648552,28.935507473821914,51.25235182574894,95.0659939561794,87.02321209227765
12,Circular lateration,53.40237345336564,44.07606633004481,44.07606633004481,30.15151517456828,47.76372684704776,90.70643165184782,82.18027326146371
12,Square lateration,47.78124737925289,40.21461923385566,40.21461923385566,25.803720681199604,44.650599346528466,84.1085157934101,76.05988162926346
13,MDS-MAP,3.0838168228627265,2.1824176770422774,2.1824176770422774,2.1787563608408704,1.616318804020763,9.572599566724628,5.940865266126406
13,Circular lateration,47.58794103662907,40.22828769028115,40.22828769028115,25.42237206898008,46.94407148106981,76.71289949529327,69.80745270801845
13,Square lateration,46.34668603920483,43.3520946182295,43.3520946182295,16.389362374073634,43.27310011910929,65.84431477484452,63.78112088505939
14,MDS-MAP,5.199427827353785,3.9022565062431633,3.9022565062431633,3.4360506240950226,2.8243087540184,15.192536389037334,10.549598789215855
14,Circular lateration,41.46548551790006,31.919655916221906,31.919655916221906,26.467377192029808,32.333376551457306,79.63666556574412,77.13283661950402
14,Square lateration,42.09475799999618,32.14012523925396,32.14012523925396,27.184204985309258,24.297664090722762,84.57357738605965,81.97083683853599
15,MDS-MAP,1.3196834826611386,1.2201643103880464,1.2201643103880464,0.5027559547771608,1.120855498796003,2.447716323126961,2.102378704758175
15,Circular lateration,60.13036542114176,55.67214534995453,55.67214534995453,22.72164337836453,57.138248206228496,92.56010223504823,87.36543253911728
15,Square lateration,39.60950104561243,37.37600779791989,37.37600779791989,13.112841575044081,36.48897512424261,63.931401275998496,55.94434970029618
16,MDS-MAP,66.39400918244505,54.80384048196851,54.80384048196851,37.47937464453679,50.348531802752454,131.29314676931065,113.98755545400145
16,Circular lateration,52.98274188678472,42.47293406073109,42.47293406073109,31.673661141624283,47.52112125027104,99.15759476797687,83.21927333026754
16,Square lateration,42.989345265532585,38.50630752392992,38.50630752392992,19.114080862853633,38.25650588703249,74.32996795543133,66.71932784643234
17,MDS-MAP,3.0582610407542914,2.693981245497273,2.693981245497273,1.4475585108397115,2.3317485080611497,6.341403276845636,5.24216799618588
17,Circular lateration,53.038800761023275,43.326313379112904,43.326313379112904,30.593217469603054,39.36155023702261,98.33996430050375,95.91140339221971
17,Square lateration,48.394089022730206,45.28952719443483,45.28952719443483,17.0542246626601,45.65228569153828,76.66926777515424,68.03509007392185
18,MDS-MAP,50.81648323314002,44.00018990046605,44.00018990046605,25.422396757719888,40.5026502176855,94.0302941222651,89.04636034890734
18,Circular lateration,50.38968903699181,38.02580520999808,38.02580520999808,33.062953579134685,32.40680701466265,103.98084517106099,91.81603543555177
18,Square lateration,44.02894711317046,36.466846993765955,36.466846993765955,24.67219597497605,31.952676915607164,88.58172204599529,72.4115601097799
19,MDS-MAP,1.9813880538858075,1.789150203258857,1.789150203258857,0.8513754578681362,1.6340018441926092,3.5978247075835905,3.3325205504004245
19,Circular lateration,39.02490480403007,32.918113142257305,32.918113142257305,20.96046330874287,30.112163833191325,76.09233791394873,61.46379632211923
19,Square lateration,37.99462125118168,32.57905132676278,32.57905132676278,19.54985060477196,32.33615842426702,69.09042188907085,60.72675858581077
20,MDS-MAP,61.63224191111844,53.15854799132299,53.15854799132299,31.18817113017105,48.36736830700242,113.509781745756,104.93827588317383
20,Circular lateration,46.90803333848101,36.404082721097424,36.404082721097424,29.5821965533248,37.34211030677703,92.71719695510292,86.89329142765163
20,Square lateration,34.27778241046017,30.02118982730481,30.02118982730481,16.543715674895484,26.374107337771324,57.6455479427844,57.31117883965203
21,MDS-MAP,60.5847543897754,51.43041663113541,51.43041663113541,32.022253350088285,47.073560590732164,122.69000685998019,121.38873199572672
21,Circular lateration,37.225869399929884,28.48838290365702,28.48838290365702,23.961998917353664,23.159239585090702,83.71874032272832,72.02194918005658
21,Square lateration,34.736442305808644,29.656712241893114,29.656712241893114,18.08590177642022,24.13421960214432,78.46165875539442,65.5218849930337
22,MDS-MAP,2.200947200025575,1.9218203208570137,1.9218203208570137,1.0727419221981882,1.8491444137833728,4.204940503082699,3.916212588435606
22,Circular lateration,40.7644238161979,31.63379802838127,31.63379802838127,25.71071899745676,29.83652674676005,84.9502592125001,70.88993286758841
22,Square lateration,42.41850774493398,36.81852830064753,36.81852830064753,21.064799383840366,40.305918757464894,76.8495999708694,67.03940151809478
23,MDS-MAP,3.211103347989262,3.075882668031813,3.075882668031813,0.9220252295731095,2.77097396367394,4.849031517294981,4.619618565136974
23,Circular lateration,53.693177933212304,48.34021378613865,48.34021378613865,23.370517488451295,50.46421754962096,97.64322600984542,77.3598723049904
23,Square lateration,47.54432223995426,42.529061699434905,42.529061699434905,21.254211070333046,42.51866965657185,90.60077168231615,74.36712913480011
24,MDS-MAP,3.3758453649322036,2.9072666830351386,2.9072666830351386,1.71584741793905,2.5625617116234314,7.254101956700976,5.883108456142343
24,Circular lateration,50.01471051173797,40.84700437293155,40.84700437293155,28.8616267963305,45.17737108990498,96.62673680815409,76.0726379944088
24,Square lateration,48.37527761305981,44.11406496019518,44.11406496019518,19.8523740854409,48.53857808471982,69.99097068057958,66.61260206938884
25,MDS-MAP,1.688195866735363,1.5291221187256046,1.5291221187256046,0.7153955762279225,1.35986608284732,3.2053757394060503,2.6430777157059695
25,Circular lateration,48.57854970451968,38.02097447621735,38.02097447621735,30.23707974116083,37.138175904440814,100.64479219243462,85.86852094984455
25,Square lateration,51.77768437386115,47.11738956207964,47.11738956207964,21.46812055058415,40.60916302219523,86.53432907821617,81.76467346782285
26,MDS-MAP,65.7106616279215,48.99871134315633,48.99871134315633,43.78375655753212,46.4656725889015,160.35701624075972,140.46459699485126
26,Circular lateration,50.930548543361965,38.49975985113552,38.49975985113552,33.342004533810545,34.82260465440932,112.57535412463584,106.1184065502898
26,Square lateration,46.90830731876349,40.77301446408061,40.77301446408061,23.19376181268218,39.58095949659456,104.69810184851322,79.6447952978983
27,MDS-MAP,63.881520230218655,52.55288270024229,52.55288270024229,36.31863360340543,47.81707409667046,113.88425855038005,104.5155874572127
27,Circular lateration,54.26294140659503,46.16665991350053,46.16665991350053,28.51501924471994,48.57902531865673,86.27533864289673,85.94010525943503
27,Square lateration,48.67524865466301,41.96787061363675,41.96787061363675,24.657203161557426,43.21481658908125,85.34970490364616,77.1843983463604
28,MDS-MAP,71.71949947564966,61.05038381153715,61.05038381153715,37.63691328339394,51.33003035295263,116.92626058392128,112.97372327432653
28,Circular lateration,54.21449099352405,43.66202756486465,43.66202756486465,32.13780301470372,41.16455401527149,114.69136410055505,90.90907556181045
28,Square lateration,40.410864637426094,35.70335532437629,35.70335532437629,18.929035879455295,32.72393362666062,68.27469321910384,65.6836031123042
29,MDS-MAP,61.93888214183786,54.69236486347834,54.69236486347834,29.071813610792166,53.91063012458599,95.30407809279569,95.13946846296074
29,Circular lateration,56.68547804599702,44.71360569988986,44.71360569988986,34.84159707329714,41.32947742288451,101.6580199083523,100.22161865932718
29,Square lateration,48.7219906646825,40.74871561397397,40.74871561397397,26.709072431308492,37.89431843737026,85.22654740570816,85.14415041484105
1 repetition method rmse mae mean std median max p95
2 0 MDS-MAP 2.185218449708254 1.8458787821819298 1.8458787821819298 1.1695773571833123 1.662450878006072 3.9786433573742683 3.6190407320796423
3 0 Circular lateration 43.78947366843516 32.206482229766934 32.206482229766934 29.669184460350156 27.674471499205733 96.88504803126659 91.33955515642728
4 0 Square lateration 44.0939637244304 34.019038921879854 34.019038921879854 28.05321064981703 21.558708106491665 92.82196959355585 84.87322026587962
5 1 MDS-MAP 104.12076076763218 87.56542088437342 87.56542088437342 56.33320413550988 117.43184088620788 154.50445738294167 154.44482717944248
6 1 Circular lateration 60.89413219933657 54.2240019721968 54.2240019721968 27.71015962475634 58.87763429444088 113.07254488237886 107.54515548490463
7 1 Square lateration 61.569113134199235 58.60133441233858 58.60133441233858 18.884896007791138 59.719879091337816 86.93801531391847 84.01272360227419
8 2 MDS-MAP 8.309204011897831 7.295218265267347 7.295218265267347 3.9777709553779834 6.952625161531805 13.65028634375217 13.581143903853613
9 2 Circular lateration 58.90527147459289 49.75408151774144 49.75408151774144 31.533512012166153 41.24813251143627 94.71083181490397 92.94763204685577
10 2 Square lateration 51.428699145885744 49.24560206341943 49.24560206341943 14.825038726740953 47.562557048841356 82.0436751907646 76.29538685792052
11 3 MDS-MAP 4.562718714542838 3.6624244679488505 3.6624244679488505 2.7212219837067737 2.9239413324889405 10.496007008331187 8.816379048148963
12 3 Circular lateration 44.731144318040506 37.08045295423461 37.08045295423461 25.01829891719581 40.092685957413664 77.21968011045794 75.33668398773148
13 3 Square lateration 40.37648518063248 36.344284636138084 36.344284636138084 17.58844876130954 32.10106382140965 67.26928046498466 60.82472560027291
14 4 MDS-MAP 37.731531242875896 28.874548437590043 28.874548437590043 24.288452039135517 25.883270592181606 88.2057668995198 69.74594678025248
15 4 Circular lateration 37.232108800794904 30.564519359637014 30.564519359637014 21.261234255531907 32.34065231775183 63.88951680632521 61.58674330967082
16 4 Square lateration 34.70763712101917 31.378417118604823 31.378417118604823 14.832903075770295 31.399199886450468 55.700989968264395 48.73261798554818
17 5 MDS-MAP 8.745454088470565 4.7373231721726405 4.7373231721726405 7.351240465114897 3.037014129702911 32.773206869979084 12.253411191399938
18 5 Circular lateration 63.12743447788416 52.88505533350713 52.88505533350713 34.470913914943516 65.61575015282035 98.6286881422611 93.52117947675772
19 5 Square lateration 52.329087777359454 49.52081173409391 49.52081173409391 16.91220366501711 53.51933906158667 84.61054214239813 73.4930051835108
20 6 MDS-MAP 2.062801521780494 1.7897094052718303 1.7897094052718303 1.0257145621182693 1.6155498003539472 3.86694194821288 3.1457997444972765
21 6 Circular lateration 43.42985452935746 34.73688498965117 34.73688498965117 26.067241619643962 30.281996396910976 80.80282359292899 78.60966287898637
22 6 Square lateration 41.53781614889254 37.147005175669584 37.147005175669584 18.58736605595249 32.928524579174955 74.69818358853158 63.912291733742556
23 7 MDS-MAP 7.329853096812777 5.806914921001663 5.806914921001663 4.472860999528612 4.329566088507324 16.554267620021584 15.136518644770634
24 7 Circular lateration 49.22768079299547 38.04624224012751 38.04624224012751 31.238566030831056 40.33077934159579 96.17515806770614 92.41138053125128
25 7 Square lateration 46.23994292584898 39.98878001130518 39.98878001130518 23.217015204224907 41.27205620569523 90.19680481386244 73.89083724702131
26 8 MDS-MAP 2.360876038225514 2.1470962365985153 2.1470962365985153 0.9816890641399094 1.9499894362935801 5.277335426175595 3.668501609696447
27 8 Circular lateration 47.55326094328507 40.008212378826016 40.008212378826016 25.703220976969288 49.25537008761242 77.2192568520714 68.68686076482805
28 8 Square lateration 46.351806700557724 41.81042386154252 41.81042386154252 20.00945878638461 33.0027993825655 79.44514858379279 74.94772560022531
29 9 MDS-MAP 57.52895101157319 47.55908308254369 47.55908308254369 32.36840775879616 52.50280885079077 123.03355597499362 91.32771520237043
30 9 Circular lateration 45.87120443806874 37.9305514989483 37.9305514989483 25.796136524385343 36.23798412456007 103.85513225229437 74.77426123066019
31 9 Square lateration 37.80148521481428 33.79777306954802 33.79777306954802 16.931119868016896 30.317145927841928 63.88645630943104 57.18777726757146
32 10 MDS-MAP 1.85488763269496 1.685840525450664 1.685840525450664 0.7736600368850265 1.3525067450922261 3.2477428625751004 2.9707277070263696
33 10 Circular lateration 58.140560551166196 48.20074053107887 48.20074053107887 32.5117423934712 47.43354721443126 114.8385863544283 94.12169641919814
34 10 Square lateration 45.98335566739533 41.83650571762525 41.83650571762525 19.083390363698168 40.74436093552332 89.45446667558086 69.05719659815932
35 11 MDS-MAP 4.374690752867252 3.642163882505203 3.642163882505203 2.4233368391944325 3.302262558017393 11.475930347382185 6.885478285992145
36 11 Circular lateration 46.32486086309956 37.53540020502426 37.53540020502426 27.1493363718931 37.14343873879638 79.62054347862745 75.6832888293246
37 11 Square lateration 40.447821876509984 32.986906372453355 32.986906372453355 23.40705668231006 31.35540088032791 80.93635896211804 75.92612956207503
38 12 MDS-MAP 53.8776564431605 45.4481932648552 45.4481932648552 28.935507473821914 51.25235182574894 95.0659939561794 87.02321209227765
39 12 Circular lateration 53.40237345336564 44.07606633004481 44.07606633004481 30.15151517456828 47.76372684704776 90.70643165184782 82.18027326146371
40 12 Square lateration 47.78124737925289 40.21461923385566 40.21461923385566 25.803720681199604 44.650599346528466 84.1085157934101 76.05988162926346
41 13 MDS-MAP 3.0838168228627265 2.1824176770422774 2.1824176770422774 2.1787563608408704 1.616318804020763 9.572599566724628 5.940865266126406
42 13 Circular lateration 47.58794103662907 40.22828769028115 40.22828769028115 25.42237206898008 46.94407148106981 76.71289949529327 69.80745270801845
43 13 Square lateration 46.34668603920483 43.3520946182295 43.3520946182295 16.389362374073634 43.27310011910929 65.84431477484452 63.78112088505939
44 14 MDS-MAP 5.199427827353785 3.9022565062431633 3.9022565062431633 3.4360506240950226 2.8243087540184 15.192536389037334 10.549598789215855
45 14 Circular lateration 41.46548551790006 31.919655916221906 31.919655916221906 26.467377192029808 32.333376551457306 79.63666556574412 77.13283661950402
46 14 Square lateration 42.09475799999618 32.14012523925396 32.14012523925396 27.184204985309258 24.297664090722762 84.57357738605965 81.97083683853599
47 15 MDS-MAP 1.3196834826611386 1.2201643103880464 1.2201643103880464 0.5027559547771608 1.120855498796003 2.447716323126961 2.102378704758175
48 15 Circular lateration 60.13036542114176 55.67214534995453 55.67214534995453 22.72164337836453 57.138248206228496 92.56010223504823 87.36543253911728
49 15 Square lateration 39.60950104561243 37.37600779791989 37.37600779791989 13.112841575044081 36.48897512424261 63.931401275998496 55.94434970029618
50 16 MDS-MAP 66.39400918244505 54.80384048196851 54.80384048196851 37.47937464453679 50.348531802752454 131.29314676931065 113.98755545400145
51 16 Circular lateration 52.98274188678472 42.47293406073109 42.47293406073109 31.673661141624283 47.52112125027104 99.15759476797687 83.21927333026754
52 16 Square lateration 42.989345265532585 38.50630752392992 38.50630752392992 19.114080862853633 38.25650588703249 74.32996795543133 66.71932784643234
53 17 MDS-MAP 3.0582610407542914 2.693981245497273 2.693981245497273 1.4475585108397115 2.3317485080611497 6.341403276845636 5.24216799618588
54 17 Circular lateration 53.038800761023275 43.326313379112904 43.326313379112904 30.593217469603054 39.36155023702261 98.33996430050375 95.91140339221971
55 17 Square lateration 48.394089022730206 45.28952719443483 45.28952719443483 17.0542246626601 45.65228569153828 76.66926777515424 68.03509007392185
56 18 MDS-MAP 50.81648323314002 44.00018990046605 44.00018990046605 25.422396757719888 40.5026502176855 94.0302941222651 89.04636034890734
57 18 Circular lateration 50.38968903699181 38.02580520999808 38.02580520999808 33.062953579134685 32.40680701466265 103.98084517106099 91.81603543555177
58 18 Square lateration 44.02894711317046 36.466846993765955 36.466846993765955 24.67219597497605 31.952676915607164 88.58172204599529 72.4115601097799
59 19 MDS-MAP 1.9813880538858075 1.789150203258857 1.789150203258857 0.8513754578681362 1.6340018441926092 3.5978247075835905 3.3325205504004245
60 19 Circular lateration 39.02490480403007 32.918113142257305 32.918113142257305 20.96046330874287 30.112163833191325 76.09233791394873 61.46379632211923
61 19 Square lateration 37.99462125118168 32.57905132676278 32.57905132676278 19.54985060477196 32.33615842426702 69.09042188907085 60.72675858581077
62 20 MDS-MAP 61.63224191111844 53.15854799132299 53.15854799132299 31.18817113017105 48.36736830700242 113.509781745756 104.93827588317383
63 20 Circular lateration 46.90803333848101 36.404082721097424 36.404082721097424 29.5821965533248 37.34211030677703 92.71719695510292 86.89329142765163
64 20 Square lateration 34.27778241046017 30.02118982730481 30.02118982730481 16.543715674895484 26.374107337771324 57.6455479427844 57.31117883965203
65 21 MDS-MAP 60.5847543897754 51.43041663113541 51.43041663113541 32.022253350088285 47.073560590732164 122.69000685998019 121.38873199572672
66 21 Circular lateration 37.225869399929884 28.48838290365702 28.48838290365702 23.961998917353664 23.159239585090702 83.71874032272832 72.02194918005658
67 21 Square lateration 34.736442305808644 29.656712241893114 29.656712241893114 18.08590177642022 24.13421960214432 78.46165875539442 65.5218849930337
68 22 MDS-MAP 2.200947200025575 1.9218203208570137 1.9218203208570137 1.0727419221981882 1.8491444137833728 4.204940503082699 3.916212588435606
69 22 Circular lateration 40.7644238161979 31.63379802838127 31.63379802838127 25.71071899745676 29.83652674676005 84.9502592125001 70.88993286758841
70 22 Square lateration 42.41850774493398 36.81852830064753 36.81852830064753 21.064799383840366 40.305918757464894 76.8495999708694 67.03940151809478
71 23 MDS-MAP 3.211103347989262 3.075882668031813 3.075882668031813 0.9220252295731095 2.77097396367394 4.849031517294981 4.619618565136974
72 23 Circular lateration 53.693177933212304 48.34021378613865 48.34021378613865 23.370517488451295 50.46421754962096 97.64322600984542 77.3598723049904
73 23 Square lateration 47.54432223995426 42.529061699434905 42.529061699434905 21.254211070333046 42.51866965657185 90.60077168231615 74.36712913480011
74 24 MDS-MAP 3.3758453649322036 2.9072666830351386 2.9072666830351386 1.71584741793905 2.5625617116234314 7.254101956700976 5.883108456142343
75 24 Circular lateration 50.01471051173797 40.84700437293155 40.84700437293155 28.8616267963305 45.17737108990498 96.62673680815409 76.0726379944088
76 24 Square lateration 48.37527761305981 44.11406496019518 44.11406496019518 19.8523740854409 48.53857808471982 69.99097068057958 66.61260206938884
77 25 MDS-MAP 1.688195866735363 1.5291221187256046 1.5291221187256046 0.7153955762279225 1.35986608284732 3.2053757394060503 2.6430777157059695
78 25 Circular lateration 48.57854970451968 38.02097447621735 38.02097447621735 30.23707974116083 37.138175904440814 100.64479219243462 85.86852094984455
79 25 Square lateration 51.77768437386115 47.11738956207964 47.11738956207964 21.46812055058415 40.60916302219523 86.53432907821617 81.76467346782285
80 26 MDS-MAP 65.7106616279215 48.99871134315633 48.99871134315633 43.78375655753212 46.4656725889015 160.35701624075972 140.46459699485126
81 26 Circular lateration 50.930548543361965 38.49975985113552 38.49975985113552 33.342004533810545 34.82260465440932 112.57535412463584 106.1184065502898
82 26 Square lateration 46.90830731876349 40.77301446408061 40.77301446408061 23.19376181268218 39.58095949659456 104.69810184851322 79.6447952978983
83 27 MDS-MAP 63.881520230218655 52.55288270024229 52.55288270024229 36.31863360340543 47.81707409667046 113.88425855038005 104.5155874572127
84 27 Circular lateration 54.26294140659503 46.16665991350053 46.16665991350053 28.51501924471994 48.57902531865673 86.27533864289673 85.94010525943503
85 27 Square lateration 48.67524865466301 41.96787061363675 41.96787061363675 24.657203161557426 43.21481658908125 85.34970490364616 77.1843983463604
86 28 MDS-MAP 71.71949947564966 61.05038381153715 61.05038381153715 37.63691328339394 51.33003035295263 116.92626058392128 112.97372327432653
87 28 Circular lateration 54.21449099352405 43.66202756486465 43.66202756486465 32.13780301470372 41.16455401527149 114.69136410055505 90.90907556181045
88 28 Square lateration 40.410864637426094 35.70335532437629 35.70335532437629 18.929035879455295 32.72393362666062 68.27469321910384 65.6836031123042
89 29 MDS-MAP 61.93888214183786 54.69236486347834 54.69236486347834 29.071813610792166 53.91063012458599 95.30407809279569 95.13946846296074
90 29 Circular lateration 56.68547804599702 44.71360569988986 44.71360569988986 34.84159707329714 41.32947742288451 101.6580199083523 100.22161865932718
91 29 Square lateration 48.7219906646825 40.74871561397397 40.74871561397397 26.709072431308492 37.89431843737026 85.22654740570816 85.14415041484105

View File

@ -0,0 +1,17 @@
node,actual_x,actual_y,mdsmap_x,mdsmap_y,mdsmap_error,circular_lateration_x,circular_lateration_y,circular_lateration_error,square_lateration_x,square_lateration_y,square_lateration_error
4,65.27848026851319,29.830276735556925,65.65635742866027,31.60002667173938,1.8096425019264493,45.22271872981485,35.18571627500968,20.758475463238277,46.72684011434851,37.050052710804806,19.90699669820593
5,96.69622001623905,91.98501605372782,94.65231618317293,94.6534381443402,3.361252643359399,61.65627696232398,53.38388660349702,52.1329531491847,42.1214436463396,41.03707451635823,74.6598885796099
6,63.587080414465134,75.27320970790866,63.89069512631613,76.41634759020968,1.1827705234761403,52.415884672274515,42.53630652523831,34.59046753517319,49.948882459523645,42.16823817304116,35.80418388655937
7,51.51536963223518,82.589525837557,51.43669862062896,83.01107965492646,0.4288318423413857,93.40686018098665,39.08755245973734,60.39303493086488,74.53976989458292,21.47352794300486,65.30917398105493
8,44.83805454001032,33.881245331376185,47.32380386998021,35.57076823060911,3.0055677264833345,46.80673119048737,37.819436381357534,4.402844137632711,46.72684011434851,37.050052710804806,3.6890176407479194
9,27.789921550015904,22.633305684340343,29.543834536888305,24.11083930947994,2.293319946919274,22.262720946818927,59.959190017735665,37.73289795096564,18.36902871671778,63.991316571515455,42.41742903949016
10,52.58168433452484,43.09120606844314,53.08388149707605,44.520824415072006,1.5152592540856946,44.47226904506267,31.951719461893354,13.778634845094956,47.142506255827556,37.050052710804806,8.128972386611114
11,66.31806204725955,1.2840482366806572,66.40619703129819,3.203587563304474,1.9215616050147621,22.05073587047681,87.46476745611605,96.88504803126659,30.436372117522012,75.26533529668862,82.22363715665422
12,44.77019015387208,36.51808011323914,48.06032430961217,37.70939461959338,3.499173190314767,81.46146959597802,59.21124163729637,43.141969902354454,65.17929821604753,46.217332217109416,22.596618845924425
13,19.539759987155403,59.486587100605284,19.165142406868064,58.607909253660154,0.955203166961077,32.59813654362682,95.50825158633785,38.31555181141581,35.49173343096512,79.0518245853265,25.244087913520477
14,43.531315595064456,29.999150374514983,46.533994145758776,32.60942325036123,3.9786433573742683,55.75639682122007,43.039898087104824,17.87494648634003,52.93283670992049,43.20300168657272,16.208957052938743
15,20.941612123621955,87.46240571921795,21.10127395440427,87.89525220510524,0.46135450637631126,90.72908375752743,31.441354488009765,89.49105753148085,82.61843345326878,18.094634024447277,92.82196959355585
16,79.74623155214327,60.6709676684979,82.57515955749263,61.70177914688235,3.0108812602656734,81.46146959597802,59.21124163729637,2.2522969682493965,65.17929821604753,46.217332217109416,20.5207973670589
17,34.51005786672921,94.68197909796484,34.000057208416735,95.61737549512173,1.0653952746719206,32.59813654362682,95.50825158633785,2.082827254184974,35.49173343096512,79.0518245853265,15.66095198263396
18,56.33773745025456,43.2762739619952,56.342564645803634,43.69818109698468,0.4219347489504768,55.75639682122007,43.039898087104824,0.6275591455762581,52.93283670992049,43.20300168657272,3.4056890459748512
19,90.0449599004222,31.93418039018624,90.19968543561002,31.330421983846723,0.6232689663899446,90.72908375752743,31.441354488009765,0.8431505332481255,82.61843345326878,18.094634024447277,15.706251579536975
1 node actual_x actual_y mdsmap_x mdsmap_y mdsmap_error circular_lateration_x circular_lateration_y circular_lateration_error square_lateration_x square_lateration_y square_lateration_error
2 4 65.27848026851319 29.830276735556925 65.65635742866027 31.60002667173938 1.8096425019264493 45.22271872981485 35.18571627500968 20.758475463238277 46.72684011434851 37.050052710804806 19.90699669820593
3 5 96.69622001623905 91.98501605372782 94.65231618317293 94.6534381443402 3.361252643359399 61.65627696232398 53.38388660349702 52.1329531491847 42.1214436463396 41.03707451635823 74.6598885796099
4 6 63.587080414465134 75.27320970790866 63.89069512631613 76.41634759020968 1.1827705234761403 52.415884672274515 42.53630652523831 34.59046753517319 49.948882459523645 42.16823817304116 35.80418388655937
5 7 51.51536963223518 82.589525837557 51.43669862062896 83.01107965492646 0.4288318423413857 93.40686018098665 39.08755245973734 60.39303493086488 74.53976989458292 21.47352794300486 65.30917398105493
6 8 44.83805454001032 33.881245331376185 47.32380386998021 35.57076823060911 3.0055677264833345 46.80673119048737 37.819436381357534 4.402844137632711 46.72684011434851 37.050052710804806 3.6890176407479194
7 9 27.789921550015904 22.633305684340343 29.543834536888305 24.11083930947994 2.293319946919274 22.262720946818927 59.959190017735665 37.73289795096564 18.36902871671778 63.991316571515455 42.41742903949016
8 10 52.58168433452484 43.09120606844314 53.08388149707605 44.520824415072006 1.5152592540856946 44.47226904506267 31.951719461893354 13.778634845094956 47.142506255827556 37.050052710804806 8.128972386611114
9 11 66.31806204725955 1.2840482366806572 66.40619703129819 3.203587563304474 1.9215616050147621 22.05073587047681 87.46476745611605 96.88504803126659 30.436372117522012 75.26533529668862 82.22363715665422
10 12 44.77019015387208 36.51808011323914 48.06032430961217 37.70939461959338 3.499173190314767 81.46146959597802 59.21124163729637 43.141969902354454 65.17929821604753 46.217332217109416 22.596618845924425
11 13 19.539759987155403 59.486587100605284 19.165142406868064 58.607909253660154 0.955203166961077 32.59813654362682 95.50825158633785 38.31555181141581 35.49173343096512 79.0518245853265 25.244087913520477
12 14 43.531315595064456 29.999150374514983 46.533994145758776 32.60942325036123 3.9786433573742683 55.75639682122007 43.039898087104824 17.87494648634003 52.93283670992049 43.20300168657272 16.208957052938743
13 15 20.941612123621955 87.46240571921795 21.10127395440427 87.89525220510524 0.46135450637631126 90.72908375752743 31.441354488009765 89.49105753148085 82.61843345326878 18.094634024447277 92.82196959355585
14 16 79.74623155214327 60.6709676684979 82.57515955749263 61.70177914688235 3.0108812602656734 81.46146959597802 59.21124163729637 2.2522969682493965 65.17929821604753 46.217332217109416 20.5207973670589
15 17 34.51005786672921 94.68197909796484 34.000057208416735 95.61737549512173 1.0653952746719206 32.59813654362682 95.50825158633785 2.082827254184974 35.49173343096512 79.0518245853265 15.66095198263396
16 18 56.33773745025456 43.2762739619952 56.342564645803634 43.69818109698468 0.4219347489504768 55.75639682122007 43.039898087104824 0.6275591455762581 52.93283670992049 43.20300168657272 3.4056890459748512
17 19 90.0449599004222 31.93418039018624 90.19968543561002 31.330421983846723 0.6232689663899446 90.72908375752743 31.441354488009765 0.8431505332481255 82.61843345326878 18.094634024447277 15.706251579536975

View File

@ -0,0 +1,20 @@
"""Run repeated experiments and save error distributions and metrics."""
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from mdsmap_sim.experiment import run_monte_carlo
from mdsmap_sim.plots import plot_error_distribution
from mdsmap_sim.utils import ensure_output_dirs, load_config
root = Path(__file__).resolve().parents[1]
config = load_config(root / "config" / "default.yaml")
figures, tables = ensure_output_dirs(root / config["output_dir"])
metrics, errors = run_monte_carlo(config)
metrics.to_csv(tables / "monte_carlo_metrics.csv", index=False)
errors.to_csv(tables / "monte_carlo_errors.csv", index=False)
plot_error_distribution(errors, figures / "error_distribution.png")
print(metrics.groupby("method").mean(numeric_only=True).to_string())

View File

@ -0,0 +1,18 @@
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from mdsmap_sim.experiment import run_monte_carlo
from mdsmap_sim.plots import plot_error_distribution
from mdsmap_sim.utils import ensure_output_dirs, load_config
root = Path(__file__).resolve().parents[1]
config = load_config(root / "config" / "default.yaml")
figures, tables = ensure_output_dirs(root / config["output_dir"])
metrics, errors = run_monte_carlo(config)
metrics.to_csv(tables / "monte_carlo_metrics.csv", index=False)
errors.to_csv(tables / "monte_carlo_errors.csv", index=False)
plot_error_distribution(errors, figures / "error_distribution.png")
print(metrics.groupby("method").mean(numeric_only=True).to_string())

20
scripts/run_single.py Normal file
View File

@ -0,0 +1,20 @@
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from mdsmap_sim.experiment import run_single_experiment
from mdsmap_sim.plots import plot_positions
from mdsmap_sim.utils import ensure_output_dirs, load_config
root = Path(__file__).resolve().parents[1]
config = load_config(root / "config" / "default.yaml")
figures, tables = ensure_output_dirs(root / config["output_dir"])
result = run_single_experiment(config)
result["node_table"].to_csv(tables / "single_node_positions.csv", index=False)
plot_positions(result, figures / "single_positions.png")
print(result["node_table"].to_string(index=False))
print("\nMetrics:")
for method, metrics in result["metrics"].items():
print(method, metrics)

16
scripts/run_sweep.py Normal file
View File

@ -0,0 +1,16 @@
"""Small noise sweep built on the main Monte Carlo pipeline."""
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from mdsmap_sim.experiment import run_monte_carlo
from mdsmap_sim.utils import load_config
root = Path(__file__).resolve().parents[1]
config = load_config(root / "config" / "default.yaml")
for sigma in (0.0, 0.5, 1.0, 2.0, 5.0):
config["noise_sigma"] = sigma
metrics, _ = run_monte_carlo(config)
summary = metrics.groupby("method")["rmse"].mean()
print(f"sigma={sigma:.1f}\n{summary.to_string()}\n")

View File

@ -0,0 +1,5 @@
"""MDS-MAP localization simulation."""
from .experiment import run_monte_carlo, run_single_experiment
__all__ = ["run_monte_carlo", "run_single_experiment"]

View File

@ -0,0 +1,55 @@
from typing import Any
import numpy as np
import pandas as pd
from .lateration import estimate_all
from .mds_map import classical_mds, procrustes_align
from .metrics import error_metrics, localization_errors
from .network import generate_network
from .ranging import measure_distances
from .shortest_path import complete_distances
def run_single_experiment(config: dict[str, Any], seed: int | None = None) -> dict[str, Any]:
rng = np.random.default_rng(config["seed"] if seed is None else seed)
area = tuple(config["area_size"])
network = generate_network(
area, config["anchor_count"], config["unknown_count"], config["radio_range"], rng
)
measured = measure_distances(
network.distances, network.adjacency, rng, config["noise_model"],
config["noise_sigma"], config["multiplicative_sigma"],
)
completed = complete_distances(measured)
mds = procrustes_align(classical_mds(completed), network.positions, network.anchors)
# The completed anchor ranges keep the baselines defined when an
# unknown is not directly connected to every anchor.
baseline_ranges = completed[np.ix_(network.unknowns, network.anchors)]
circular, square = estimate_all(
network.positions, network.anchors, network.unknowns, baseline_ranges
)
actual = network.positions[network.unknowns]
estimates = {"MDS-MAP": mds[network.unknowns], "Circular lateration": circular, "Square lateration": square}
errors = {name: localization_errors(actual, estimate) for name, estimate in estimates.items()}
rows = []
for node_offset, node in enumerate(network.unknowns):
row = {"node": int(node), "actual_x": actual[node_offset, 0], "actual_y": actual[node_offset, 1]}
for name, estimate in estimates.items():
key = name.lower().replace(" ", "_").replace("-", "")
row.update({f"{key}_x": estimate[node_offset, 0], f"{key}_y": estimate[node_offset, 1],
f"{key}_error": errors[name][node_offset]})
rows.append(row)
return {"network": network, "estimates": estimates, "errors": errors, "node_table": pd.DataFrame(rows),
"metrics": {name: error_metrics(values) for name, values in errors.items()}}
def run_monte_carlo(config: dict[str, Any]) -> tuple[pd.DataFrame, pd.DataFrame]:
metric_rows, error_rows = [], []
for repetition in range(int(config["repetitions"])):
result = run_single_experiment(config, int(config["seed"]) + repetition)
for method, metrics in result["metrics"].items():
metric_rows.append({"repetition": repetition, "method": method, **metrics})
error_rows.extend({"repetition": repetition, "method": method, "error": error}
for error in result["errors"][method])
return pd.DataFrame(metric_rows), pd.DataFrame(error_rows)

View File

@ -0,0 +1,47 @@
import numpy as np
from scipy.optimize import least_squares
def circular_lateration(
anchors: np.ndarray, anchor_indices: np.ndarray, distances: np.ndarray
) -> np.ndarray:
"""Estimate a point from anchor ranges by nonlinear least squares."""
if len(anchor_indices) < 3:
raise ValueError("Circular lateration requires at least 3 anchors.")
anchor_points = anchors[anchor_indices]
initial = anchor_points.mean(axis=0)
def residual(point: np.ndarray) -> np.ndarray:
return np.linalg.norm(point - anchor_points, axis=1) - distances
return least_squares(residual, initial).x
def square_lateration(
anchors: np.ndarray, anchor_indices: np.ndarray, distances: np.ndarray
) -> np.ndarray:
"""Min-max (square intersection) lateration using anchor distance bounds."""
if len(anchor_indices) < 3:
raise ValueError("Square lateration requires at least 3 anchors.")
anchor_points = anchors[anchor_indices]
lower = np.max(anchor_points - distances[:, None], axis=0)
upper = np.min(anchor_points + distances[:, None], axis=0)
if np.any(lower > upper):
return np.mean(anchor_points, axis=0)
return (lower + upper) / 2.0
def estimate_all(
positions: np.ndarray,
anchor_indices: np.ndarray,
unknown_indices: np.ndarray,
measured_distances: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
anchor_points = positions[anchor_indices]
circular, square = [], []
for offset, node in enumerate(unknown_indices):
row = node if measured_distances.shape[0] > node else offset
ranges = measured_distances[row, anchor_indices]
circular.append(circular_lateration(anchor_points, np.arange(len(anchor_points)), ranges))
square.append(square_lateration(anchor_points, np.arange(len(anchor_points)), ranges))
return np.asarray(circular), np.asarray(square)

28
src/mdsmap_sim/mds_map.py Normal file
View File

@ -0,0 +1,28 @@
import numpy as np
def classical_mds(distances: np.ndarray, dimensions: int = 2) -> np.ndarray:
n = distances.shape[0]
centering = np.eye(n) - np.ones((n, n)) / n
gram = -0.5 * centering @ (distances**2) @ centering
eigenvalues, eigenvectors = np.linalg.eigh((gram + gram.T) / 2)
order = np.argsort(eigenvalues)[::-1]
eigenvalues = np.maximum(eigenvalues[order[:dimensions]], 0.0)
return eigenvectors[:, order[:dimensions]] * np.sqrt(eigenvalues)
def procrustes_align(
estimated: np.ndarray, reference: np.ndarray, indices: np.ndarray
) -> np.ndarray:
"""Align an MDS map to known anchors using a similarity transform."""
x = estimated[indices]
y = reference[indices]
x_mean, y_mean = x.mean(axis=0), y.mean(axis=0)
xc, yc = x - x_mean, y - y_mean
u, singular_values, vt = np.linalg.svd(xc.T @ yc)
rotation = u @ vt
if np.linalg.det(rotation) < 0:
vt[-1, :] *= -1
rotation = u @ vt
scale = singular_values.sum() / max(np.sum(xc**2), 1e-12)
return (estimated - x_mean) @ rotation * scale + y_mean

18
src/mdsmap_sim/metrics.py Normal file
View File

@ -0,0 +1,18 @@
import numpy as np
def localization_errors(actual: np.ndarray, estimated: np.ndarray) -> np.ndarray:
return np.linalg.norm(actual - estimated, axis=1)
def error_metrics(errors: np.ndarray) -> dict[str, float]:
errors = np.asarray(errors, dtype=float)
return {
"rmse": float(np.sqrt(np.mean(errors**2))),
"mae": float(np.mean(np.abs(errors))),
"mean": float(np.mean(errors)),
"std": float(np.std(errors)),
"median": float(np.median(errors)),
"max": float(np.max(errors)),
"p95": float(np.percentile(errors, 95)),
}

33
src/mdsmap_sim/network.py Normal file
View File

@ -0,0 +1,33 @@
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class Network:
positions: np.ndarray
anchors: np.ndarray
unknowns: np.ndarray
adjacency: np.ndarray
distances: np.ndarray
def generate_network(
area_size: tuple[float, float],
anchor_count: int,
unknown_count: int,
radio_range: float,
rng: np.random.Generator,
) -> Network:
if anchor_count < 3 or unknown_count < 5:
raise ValueError("At least 3 anchors and 5 unknown nodes are required.")
positions = rng.uniform([0.0, 0.0], area_size, size=(anchor_count + unknown_count, 2))
distances = np.linalg.norm(positions[:, None, :] - positions[None, :, :], axis=2)
adjacency = (distances <= radio_range) & ~np.eye(len(positions), dtype=bool)
return Network(
positions=positions,
anchors=np.arange(anchor_count),
unknowns=np.arange(anchor_count, anchor_count + unknown_count),
adjacency=adjacency,
distances=distances,
)

36
src/mdsmap_sim/plots.py Normal file
View File

@ -0,0 +1,36 @@
from pathlib import Path
import matplotlib.pyplot as plt
import pandas as pd
def plot_positions(result: dict, path: str | Path) -> None:
network, estimates = result["network"], result["estimates"]
fig, ax = plt.subplots(figsize=(8, 7))
ax.scatter(network.positions[network.unknowns, 0], network.positions[network.unknowns, 1],
marker="o", label="Actual unknowns", color="black")
ax.scatter(network.positions[network.anchors, 0], network.positions[network.anchors, 1],
marker="^", s=90, label="Anchors", color="tab:green")
styles = {"MDS-MAP": ("tab:blue", "x"), "Circular lateration": ("tab:orange", "+"),
"Square lateration": ("tab:red", "s")}
for method, estimate in estimates.items():
color, marker = styles[method]
ax.scatter(estimate[:, 0], estimate[:, 1], marker=marker, color=color, label=method)
ax.set(xlabel="x (m)", ylabel="y (m)", title="Actual and estimated node positions")
ax.legend()
ax.grid(alpha=0.25)
fig.tight_layout()
fig.savefig(path, dpi=160)
plt.close(fig)
def plot_error_distribution(errors: pd.DataFrame, path: str | Path) -> None:
fig, ax = plt.subplots(figsize=(8, 5))
for method, group in errors.groupby("method"):
ax.hist(group["error"], bins=20, alpha=0.55, label=method)
ax.set(xlabel="Localization error (m)", ylabel="Node count", title="Localization error distribution")
ax.legend()
ax.grid(alpha=0.25)
fig.tight_layout()
fig.savefig(path, dpi=160)
plt.close(fig)

27
src/mdsmap_sim/ranging.py Normal file
View File

@ -0,0 +1,27 @@
import numpy as np
def measure_distances(
distances: np.ndarray,
adjacency: np.ndarray,
rng: np.random.Generator,
model: str = "gaussian",
sigma: float = 1.0,
multiplicative_sigma: float = 0.02,
) -> np.ndarray:
"""Measure connected links and represent unavailable links by infinity."""
measured = np.full_like(distances, np.inf, dtype=float)
np.fill_diagonal(measured, 0.0)
links = adjacency.astype(bool)
if model == "gaussian":
noise = rng.normal(0.0, sigma, size=distances.shape)
values = distances + noise
elif model == "multiplicative":
values = distances * (1.0 + rng.normal(0.0, multiplicative_sigma, size=distances.shape))
elif model == "none":
values = distances.copy()
else:
raise ValueError(f"Unknown noise model: {model}")
measured[links] = np.maximum(values[links], 1e-9)
measured[links] = measured[links.T]
return measured

View File

@ -0,0 +1,9 @@
import numpy as np
from scipy.sparse.csgraph import shortest_path
def complete_distances(measured_distances: np.ndarray) -> np.ndarray:
completed = shortest_path(measured_distances, directed=False, unweighted=False)
if not np.isfinite(completed).all():
raise ValueError("Network is disconnected; increase radio_range or retry.")
return completed

22
src/mdsmap_sim/utils.py Normal file
View File

@ -0,0 +1,22 @@
from pathlib import Path
from typing import Any
import yaml
def load_config(path: str | Path) -> dict[str, Any]:
"""Load a YAML configuration and fail clearly for malformed files."""
with Path(path).open(encoding="utf-8") as handle:
config = yaml.safe_load(handle)
if not isinstance(config, dict):
raise ValueError(f"Configuration must be a mapping: {path}")
return config
def ensure_output_dirs(root: str | Path) -> tuple[Path, Path]:
root_path = Path(root)
figures = root_path / "figures"
tables = root_path / "tables"
figures.mkdir(parents=True, exist_ok=True)
tables.mkdir(parents=True, exist_ok=True)
return figures, tables

13
tests/test_lateration.py Normal file
View File

@ -0,0 +1,13 @@
import numpy as np
from mdsmap_sim.lateration import circular_lateration, square_lateration
def test_lateration_noise_free():
anchors = np.array([[0, 0], [10, 0], [0, 10], [10, 10]], dtype=float)
target = np.array([4.0, 6.0])
distances = np.linalg.norm(anchors - target, axis=1)
assert np.linalg.norm(circular_lateration(anchors, np.arange(4), distances) - target) < 1e-7
estimate = square_lateration(anchors, np.arange(4), distances)
assert np.all(np.isfinite(estimate))
assert np.linalg.norm(estimate - target) < 1.0

10
tests/test_mds_map.py Normal file
View File

@ -0,0 +1,10 @@
import numpy as np
from mdsmap_sim.mds_map import classical_mds, procrustes_align
def test_noise_free_mds_alignment_is_exact():
points = np.array([[0, 0], [10, 0], [0, 10], [7, 4], [3, 8]], dtype=float)
distances = np.linalg.norm(points[:, None] - points[None, :], axis=2)
aligned = procrustes_align(classical_mds(distances), points, np.array([0, 1, 2]))
assert np.max(np.linalg.norm(aligned - points, axis=1)) < 1e-8

10
tests/ttest_metrics.py Normal file
View File

@ -0,0 +1,10 @@
import numpy as np
from mdsmap_sim.metrics import error_metrics
def test_error_metrics():
metrics = error_metrics(np.array([1.0, 2.0, 3.0]))
assert metrics["mae"] == 2.0
assert metrics["median"] == 2.0
assert metrics["max"] == 3.0