Prvy
This commit is contained in:
commit
6614ac837e
28
README.md
Normal file
28
README.md
Normal 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
10
config/default.yaml
Normal 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
14
docs/report.md
Normal 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
0
notebooks/analysis.ipynb
Normal file
7
requirements.txt
Normal file
7
requirements.txt
Normal 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
|
||||||
BIN
results/figures/error_distribution.png
Normal file
BIN
results/figures/error_distribution.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
results/figures/single_positions.png
Normal file
BIN
results/figures/single_positions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
1441
results/tables/monte_carlo_errors.csv
Normal file
1441
results/tables/monte_carlo_errors.csv
Normal file
File diff suppressed because it is too large
Load Diff
91
results/tables/monte_carlo_metrics.csv
Normal file
91
results/tables/monte_carlo_metrics.csv
Normal 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
|
||||||
|
17
results/tables/single_node_positions.csv
Normal file
17
results/tables/single_node_positions.csv
Normal 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
|
||||||
|
20
scripts/run_monte_carlo.py
Normal file
20
scripts/run_monte_carlo.py
Normal 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())
|
||||||
18
scripts/run_remote_carlo.py
Normal file
18
scripts/run_remote_carlo.py
Normal 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
20
scripts/run_single.py
Normal 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
16
scripts/run_sweep.py
Normal 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")
|
||||||
5
src/mdsmap_sim/__init__.py
Normal file
5
src/mdsmap_sim/__init__.py
Normal 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"]
|
||||||
55
src/mdsmap_sim/experiment.py
Normal file
55
src/mdsmap_sim/experiment.py
Normal 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)
|
||||||
47
src/mdsmap_sim/lateration.py
Normal file
47
src/mdsmap_sim/lateration.py
Normal 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
28
src/mdsmap_sim/mds_map.py
Normal 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
18
src/mdsmap_sim/metrics.py
Normal 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
33
src/mdsmap_sim/network.py
Normal 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
36
src/mdsmap_sim/plots.py
Normal 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
27
src/mdsmap_sim/ranging.py
Normal 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
|
||||||
9
src/mdsmap_sim/shortest_path.py
Normal file
9
src/mdsmap_sim/shortest_path.py
Normal 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
22
src/mdsmap_sim/utils.py
Normal 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
13
tests/test_lateration.py
Normal 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
10
tests/test_mds_map.py
Normal 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
10
tests/ttest_metrics.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user