diff --git a/.gitignore b/.gitignore index dd750cb..9091e25 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ __pycache__/ *.db *.pkl app.log +src/nopywer/ortools_solverV1_with_inventory.py +*.csv +*.pdf +telemetry data/PV Investment estimation.xlsx +optimizer.log diff --git a/README.md b/README.md index d58a6df..c030c65 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ export NOPYWER_INPUT=input.geojson export NOPYWER_OUTPUT=output.geojson export NOPYWER_INVENTORY=inventory.xlsx nopywer-analyze + +They can be also included in a .env file at parent directory. ``` To start the companion API server: diff --git a/nopywer_export.geojson b/nopywer_export.geojson new file mode 100644 index 0000000..fa86cd1 --- /dev/null +++ b/nopywer_export.geojson @@ -0,0 +1,984 @@ +{ + "type": "FeatureCollection", + "crs": { + "type": "name", + "properties": { + "name": "EPSG:4326" + } + }, + "power_unit": "W", + "features": [ + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14046027066882047, + 41.70048109809566 + ], + "type": "Point" + }, + "properties": { + "fid": 1, + "power": 1000.0, + "phase": "2", + "name": "Mini Camp" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1403126048615206, + 41.70021165965277 + ], + "type": "Point" + }, + "properties": { + "fid": 2, + "power": 1000.0, + "phase": "3", + "name": "EYAO" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13986458645846486, + 41.70012838821719 + ], + "type": "Point" + }, + "properties": { + "fid": 3, + "power": 1000.0, + "phase": "2", + "name": "Glitch" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14001451226101178, + 41.69988172981668 + ], + "type": "Point" + }, + "properties": { + "fid": 4, + "power": 1000.0, + "phase": "1", + "name": "Belly Town" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14032067533151324, + 41.699387585580126 + ], + "type": "Point" + }, + "properties": { + "fid": 5, + "power": 1000.0, + "phase": "1", + "name": "PDR of Fiascostan" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14037095139108816, + 41.69906229826065 + ], + "type": "Point" + }, + "properties": { + "fid": 6, + "power": 1000.0, + "phase": "2", + "name": "Mirage" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1402769285712588, + 41.69884015144679 + ], + "type": "Point" + }, + "properties": { + "fid": 7, + "power": 1400.0, + "phase": "3", + "name": "Wonderever" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13983096674250983, + 41.698769513641636 + ], + "type": "Point" + }, + "properties": { + "fid": 8, + "power": 3000.0, + "phase": "1", + "name": "Oasis Playground" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1394393620604287, + 41.69898872265349 + ], + "type": "Point" + }, + "properties": { + "fid": 9, + "power": 1000.0, + "phase": "2", + "name": "Planet Pulpo" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1400824723373084, + 41.699631722152795 + ], + "type": "Point" + }, + "properties": { + "fid": 10, + "power": 1000.0, + "phase": "3", + "name": "Curious Creatures" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1403046266028607, + 41.700765329482905 + ], + "type": "Point" + }, + "properties": { + "fid": 11, + "power": 1000.0, + "phase": "1", + "name": "Glowy Owl" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14061462479929102, + 41.701104168536816 + ], + "type": "Point" + }, + "properties": { + "fid": 12, + "power": 1000.0, + "phase": "2", + "name": "\nBarrio Espace\n" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14105640391758656, + 41.70179263095132 + ], + "type": "Point" + }, + "properties": { + "fid": 13, + "power": 4000.0, + "phase": "3", + "name": "Ran'Dome" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14100409056756252, + 41.70136886602274 + ], + "type": "Point" + }, + "properties": { + "fid": 14, + "power": 6800.0, + "phase": "1", + "name": "Desert Dessert" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1407273698750896, + 41.701997572463505 + ], + "type": "Point" + }, + "properties": { + "fid": 15, + "power": 20000.0, + "phase": "2", + "name": "Garden of Joy" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13944698019201643, + 41.70162190531119 + ], + "type": "Point" + }, + "properties": { + "fid": 17, + "power": 3680.0, + "phase": "1", + "name": "Barrio del Sol" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1391394682682699, + 41.69927405035179 + ], + "type": "Point" + }, + "properties": { + "fid": 19, + "power": 1000.0, + "phase": "2,3", + "name": "Olive Odissey" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13952647308927593, + 41.69998517673939 + ], + "type": "Point" + }, + "properties": { + "fid": 21, + "power": 1000.0, + "phase": "2", + "name": "distro9" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1398948350326917, + 41.70057458012552 + ], + "type": "Point" + }, + "properties": { + "fid": 22, + "power": 0.0, + "phase": "", + "name": "generator" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13987204105721388, + 41.70115113523339 + ], + "type": "Point" + }, + "properties": { + "fid": 23, + "power": 0.0, + "phase": "", + "name": "distro1" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14052384215401315, + 41.70162166285516 + ], + "type": "Point" + }, + "properties": { + "fid": 24, + "power": 1000.0, + "phase": "2", + "name": "ZAZ" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13961203403388528, + 41.70130951714179 + ], + "type": "Point" + }, + "properties": { + "fid": 25, + "power": 1000.0, + "phase": "3", + "name": "RDTFGH" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13989622438430727, + 41.69911385446002 + ], + "type": "Point" + }, + "properties": { + "fid": 26, + "power": 1000.0, + "phase": "1", + "name": "distro8" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13951008153297134, + 41.70022595079415 + ], + "type": "Point" + }, + "properties": { + "fid": 28, + "power": 0.0, + "phase": "", + "name": "distro2" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.13988830557411927, + 41.701143908996386 + ], + "type": "Point" + }, + "properties": { + "fid": 30, + "power": 0.0, + "phase": null, + "name": "distro" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.14040007075315172, + 41.69928240812505 + ], + "type": "Point" + }, + "properties": { + "fid": 31, + "power": 0.0, + "phase": null, + "name": "JamHouse" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + -0.1399063474789402, + 41.69912586313198 + ], + "type": "Point" + }, + "properties": { + "fid": 32, + "power": 0.0, + "phase": null, + "name": "distro6" + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.1358634919732599, + 41.700722885105534 + ], + [ + -0.13589487185959379, + 41.700491356829055 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 38, + "id": null, + "notes": null, + "phase": 2, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13721678687118344, + 41.69897332773713 + ], + [ + -0.13723473774240005, + 41.699480301357305 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 50, + "id": null, + "notes": null, + "phase": 3, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.136946721189126, + 41.70058175298905 + ], + [ + -0.13696022057941495, + 41.70077074108049 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 68, + "id": null, + "notes": null, + "phase": 1, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13775783580145348, + 41.701017637026574 + ], + [ + -0.1377138674373942, + 41.701476697407855 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 73, + "id": null, + "notes": null, + "phase": 1, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13776074711101063, + 41.70040172314188 + ], + [ + -0.1383265065243817, + 41.70047734086633 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 77, + "id": null, + "notes": null, + "phase": 3, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.1383660635519354, + 41.701110669984104 + ], + [ + -0.13867822792326492, + 41.70159538737178 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 78, + "id": null, + "notes": null, + "phase": 2, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13647667037934735, + 41.700389376705765 + ], + [ + -0.13654311634408184, + 41.70064621259186 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 80, + "id": null, + "notes": null, + "phase": 2, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.1367241540902938, + 41.70023576191971 + ], + [ + -0.13647580663116396, + 41.70038758805697 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 82, + "id": null, + "notes": null, + "phase": 2, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13826592688444675, + 41.701472011492505 + ], + [ + -0.13836265394923009, + 41.70111121926144 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 89, + "id": null, + "notes": null, + "phase": 3, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13749588811049746, + 41.699105845853566 + ], + [ + -0.13723239955287345, + 41.69947916905125 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 90, + "id": null, + "notes": null, + "phase": 1, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.1357408714998589, + 41.700355165532265 + ], + [ + -0.13589549609553478, + 41.70048969150146 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 91, + "id": null, + "notes": null, + "phase": 2, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13744505973057222, + 41.70067635384229 + ], + [ + -0.13711069462072983, + 41.700737144769114 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 92, + "id": null, + "notes": null, + "phase": null, + "area": 2.5, + "plugs&sockets": 16.0, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.1377555218615635, + 41.700402571544025 + ], + [ + -0.1380753282011498, + 41.700557438031595 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 93, + "id": null, + "notes": null, + "phase": null, + "area": null, + "plugs&sockets": null, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13775686180885907, + 41.700402604944074 + ], + [ + -0.13793216684153273, + 41.7001356003461 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 94, + "id": null, + "notes": null, + "phase": null, + "area": null, + "plugs&sockets": null, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.1377575602537182, + 41.7004026716304 + ], + [ + -0.13767942576414527, + 41.70010668154157 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 95, + "id": null, + "notes": null, + "phase": null, + "area": null, + "plugs&sockets": null, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.1377578671371608, + 41.701016930964016 + ], + [ + -0.13748051264301162, + 41.70142233907045 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 96, + "id": null, + "notes": null, + "phase": null, + "area": null, + "plugs&sockets": null, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13738260788357232, + 41.70126408080498 + ], + [ + -0.13776072486584146, + 41.70101629535332 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 97, + "id": null, + "notes": null, + "phase": null, + "area": null, + "plugs&sockets": null, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13733529820089352, + 41.701104171330584 + ], + [ + -0.1377578671371608, + 41.701016930964016 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 98, + "id": null, + "notes": null, + "phase": null, + "area": null, + "plugs&sockets": null, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.13743084611499873, + 41.70096529812862 + ], + [ + -0.13775601421089068, + 41.70101617793353 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 99, + "id": null, + "notes": null, + "phase": null, + "area": null, + "plugs&sockets": null, + "length": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [ + -0.1367370904032387, + 41.70022974030239 + ], + [ + -0.13661579870963406, + 41.69984596268292 + ] + ] + ], + "type": "MultiLineString" + }, + "properties": { + "fid": 100, + "id": null, + "notes": null, + "phase": null, + "area": null, + "plugs&sockets": null, + "length": 0.0 + } + } + ] +} \ No newline at end of file diff --git a/nopywer_export.geojson.png b/nopywer_export.geojson.png new file mode 100644 index 0000000..10dd890 Binary files /dev/null and b/nopywer_export.geojson.png differ diff --git a/nopywer_output_9evk5ki_.geojson.png b/nopywer_output_9evk5ki_.geojson.png new file mode 100644 index 0000000..a488e71 Binary files /dev/null and b/nopywer_output_9evk5ki_.geojson.png differ diff --git a/optimizer.log b/optimizer.log new file mode 100644 index 0000000..f45b634 --- /dev/null +++ b/optimizer.log @@ -0,0 +1,82 @@ +05-18 18:52 nopywer.optimize_mixed INFO [HUB] scores: [('distro2', 48100.0), ('distro8', 37430.0), ('malfare', 31100.0), ('barrio support', 30770.0), ('le gratin', 25820.0)] +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro9: 35.0mm² (95.4A, 46m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro4: 2.5mm² (7.5A, 73m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro1: 6.0mm² (22.9A, 39m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro5-1: 35.0mm² (82.1A, 90m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro3: 16.0mm² (50.8A, 45m) +05-18 18:52 nopywer.optimize_mixed WARNING [TIER] No suitable tier for generator\u2192distro8: flow=26830W, current=129.6A, d=88m — using largest tier (35.0mm²) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro8: 35.0mm² (129.6A, 88m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro7: 2.5mm² (0.0A, 87m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro6-1: 16.0mm² (22.6A, 129m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] generator\u2192distro2: 35.0mm² (96.0A, 57m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro9\u2192artinstallation1: 2.5mm² (4.8A, 21m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro9\u2192noinfo: 6.0mm² (23.9A, 26m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro9\u2192mon: 35.0mm² (66.7A, 52m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro4\u2192ohana haus: 2.5mm² (0.3A, 29m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro4\u2192peach please!: 2.5mm² (7.2A, 44m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro1\u2192kunsthaus: 6.0mm² (22.9A, 13m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro5-1\u2192jugaton: 2.5mm² (4.8A, 41m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro5-1\u2192mooncows: 6.0mm² (14.5A, 52m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro5-1\u2192camp cambio: 16.0mm² (62.8A, 61m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro3\u2192le gratin: 2.5mm² (14.5A, 36m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro3\u2192pink poney paradise: 2.5mm² (4.8A, 49m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro3\u2192dusty dreamers: 2.5mm² (4.8A, 60m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro3\u2192nobarrio: 6.0mm² (26.7A, 61m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro8\u2192la cantina: 16.0mm² (50.7A, 18m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro8\u2192production: 16.0mm² (62.8A, 24m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro8\u2192toolhaus: 6.0mm² (16.1A, 43m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro6-1\u2192hyperflow: 2.5mm² (4.8A, 53m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro6-1\u2192here & meow: 6.0mm² (17.8A, 54m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro2\u2192malfare: 16.0mm² (33.8A, 13m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro2\u2192werkhaus: 6.0mm² (25.5A, 22m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro2\u2192barrio support: 6.0mm² (17.4A, 27m) +05-18 18:52 nopywer.optimize_mixed DEBUG [TIER] distro2\u2192welfare: 6.0mm² (19.3A, 27m) +05-18 18:52 nopywer.optimize_mixed DEBUG Assigned tiers from MST: {('generator', 'distro9'): , ('generator', 'distro4'): , ('generator', 'distro1'): , ('generator', 'distro5-1'): , ('generator', 'distro3'): , ('generator', 'distro8'): , ('generator', 'distro7'): , ('generator', 'distro6-1'): , ('generator', 'distro2'): , ('distro9', 'artinstallation1'): , ('distro9', 'noinfo'): , ('distro9', 'mon'): , ('distro4', 'ohana haus'): , ('distro4', 'peach please!'): , ('distro1', 'kunsthaus'): , ('distro5-1', 'jugaton'): , ('distro5-1', 'mooncows'): , ('distro5-1', 'camp cambio'): , ('distro3', 'le gratin'): , ('distro3', 'pink poney paradise'): , ('distro3', 'dusty dreamers'): , ('distro3', 'nobarrio'): , ('distro8', 'la cantina'): , ('distro8', 'production'): , ('distro8', 'toolhaus'): , ('distro6-1', 'hyperflow'): , ('distro6-1', 'here & meow'): , ('distro2', 'malfare'): , ('distro2', 'werkhaus'): , ('distro2', 'barrio support'): , ('distro2', 'welfare'): } +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] distro8: 26830W \u2192 L0=8943W, L1=8943W, L2=8943W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] distro2: 19880W \u2192 L0=6627W, L1=6627W, L2=6627W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] distro9: 19740W \u2192 L0=6580W, L1=6580W, L2=6580W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] distro5-1: 17000W \u2192 L0=5667W, L1=5667W, L2=5667W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] distro3: 10520W \u2192 L0=5260W, L1=5260W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] distro1: 4730W \u2192 L2=4730W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] distro6-1: 4680W \u2192 L2=4680W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] production: 13000W \u2192 L0=4333W, L1=4333W, L2=4333W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] la cantina: 10490W \u2192 L1=5245W, L0=5245W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] mon: 13800W \u2192 L0=4600W, L1=4600W, L2=4600W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE SPLIT] camp cambio: 13000W \u2192 L0=4333W, L1=4333W, L2=4333W +05-18 18:52 nopywer.optimize_mixed INFO [PHASE] L0: 35462W (34.3%) +05-18 18:52 nopywer.optimize_mixed INFO [PHASE] L1: 36192W (35.0%) +05-18 18:52 nopywer.optimize_mixed INFO [PHASE] L2: 31727W (30.7%) +05-18 18:52 nopywer.optimize_mixed INFO [PHASE] imbalance: 4465W (4.3%) +05-18 18:52 nopywer.analyze INFO compute_distro_requirements... +05-18 18:52 nopywer.analyze DEBUG noinfo: +05-18 18:52 nopywer.analyze DEBUG mon: +05-18 18:52 nopywer.analyze DEBUG malfare: +05-18 18:52 nopywer.analyze DEBUG kunsthaus: +05-18 18:52 nopywer.analyze DEBUG dusty dreamers: +05-18 18:52 nopywer.analyze DEBUG mooncows: +05-18 18:52 nopywer.analyze DEBUG jugaton: +05-18 18:52 nopywer.analyze DEBUG camp cambio: +05-18 18:52 nopywer.analyze DEBUG la cantina: +05-18 18:52 nopywer.analyze DEBUG toolhaus: +05-18 18:52 nopywer.analyze DEBUG production: +05-18 18:52 nopywer.analyze DEBUG peach please!: +05-18 18:52 nopywer.analyze DEBUG here & meow: +05-18 18:52 nopywer.analyze DEBUG hyperflow: +05-18 18:52 nopywer.analyze DEBUG generator: +05-18 18:52 nopywer.analyze DEBUG distro9: +05-18 18:52 nopywer.analyze DEBUG distro4: +05-18 18:52 nopywer.analyze DEBUG distro1: +05-18 18:52 nopywer.analyze DEBUG distro5-1: +05-18 18:52 nopywer.analyze DEBUG distro3: +05-18 18:52 nopywer.analyze DEBUG werkhaus: +05-18 18:52 nopywer.analyze DEBUG nobarrio: +05-18 18:52 nopywer.analyze DEBUG pink poney paradise: +05-18 18:52 nopywer.analyze DEBUG le gratin: +05-18 18:52 nopywer.analyze DEBUG distro8: +05-18 18:52 nopywer.analyze DEBUG distro7: +05-18 18:52 nopywer.analyze DEBUG distro6-1: +05-18 18:52 nopywer.analyze DEBUG artinstallation1: +05-18 18:52 nopywer.analyze DEBUG ohana haus: +05-18 18:52 nopywer.analyze DEBUG welfare: +05-18 18:52 nopywer.analyze DEBUG barrio support: +05-18 18:52 nopywer.analyze DEBUG distro2: diff --git a/orange_inventory.xlsx b/orange_inventory.xlsx new file mode 100644 index 0000000..dd615eb Binary files /dev/null and b/orange_inventory.xlsx differ diff --git a/pyproject.toml b/pyproject.toml index b9823d2..59706ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,8 @@ dependencies = [ "pyproj>=3.7", "typer>=0.24", "uvicorn>=0.41", + "ortools", + "openpyxl", ] [project.optional-dependencies] diff --git a/src/nopywer/analyze.py b/src/nopywer/analyze.py index 62738ef..96c0fa7 100644 --- a/src/nopywer/analyze.py +++ b/src/nopywer/analyze.py @@ -154,7 +154,7 @@ def _compute_voltage_drop(grid: PowerGrid, node_name: str | None = None) -> None logger.debug(f"\t\t vdrop: {node.vdrop_percent:.1f}%") if node.vdrop_percent > VDROP_THRESHOLD_PERCENT: - logger.info(f"\t /!\\ vdrop of {node.vdrop_percent:.1f} percent at {node_name}") + logger.warning(f"\t /!\\ vdrop of {node.vdrop_percent:.1f} percent at {node_name}") for child_name in node.children: _compute_voltage_drop(grid, child_name) diff --git a/src/nopywer/api.py b/src/nopywer/api.py index fe42628..7289031 100644 --- a/src/nopywer/api.py +++ b/src/nopywer/api.py @@ -8,7 +8,11 @@ from .constants import EXTRA_CABLE_LENGTH_M from .io import load_geojson from .models import PowerGrid -from .optimize import optimize_layout + +# from .optimize import optimize_layout +from .ortools_solverV1 import optimize_layout +from .optimize_mixed import optimize_layout + app = FastAPI(title="nopywer", version="1.0.0") frontend_dir = Path(__file__).resolve().parent / "frontend" @@ -35,12 +39,15 @@ async def disable_frontend_cache(request: Request, call_next): class OptimizeRequest(BaseModel): nodes_geojson: dict extra_cable_m: float = EXTRA_CABLE_LENGTH_M + hub_discount: float = 0.7 + radiality_factor: float = 0.3 class OptimizeResponse(BaseModel): cables_geojson: dict total_cable_length_m: float num_cables: int + phase_loads: dict[int, float] @app.post("/api/v1/optimize", response_model=OptimizeResponse) @@ -58,7 +65,10 @@ def optimize(req: OptimizeRequest): if not loads: raise HTTPException(400, "At least one load is required") - grid = optimize_layout(grid, extra_cable_m=req.extra_cable_m) + grid = optimize_layout(grid, extra_cable_m=req.extra_cable_m, + hub_discount=req.hub_discount, + radiality_factor=req.radiality_factor, + ) return OptimizeResponse( cables_geojson=grid.to_geojson(), @@ -67,6 +77,7 @@ def optimize(req: OptimizeRequest): 1, ), num_cables=len(grid.cables), + phase_loads=grid.phase_loads, ) diff --git a/src/nopywer/cli.py b/src/nopywer/cli.py index 06e0a26..ca8b0b8 100644 --- a/src/nopywer/cli.py +++ b/src/nopywer/cli.py @@ -45,12 +45,14 @@ def analyze_grid( if verbose: logging.basicConfig(level=logging.INFO) - grid = PowerGrid.from_geojson(input) + if type(input) is str: + grid = PowerGrid.from_geojson(input) + elif type(input) is PowerGrid: + grid = input analyze(grid) if verbose: print_grid_info(grid.nodes, grid.cables, grid.tree, grid.generator) - if inventory_file: inventory.choose_cables(inventory_file, grid.cables) inventory.choose_distros(inventory_file, grid.nodes) diff --git a/src/nopywer/debugger.py b/src/nopywer/debugger.py new file mode 100644 index 0000000..c904278 --- /dev/null +++ b/src/nopywer/debugger.py @@ -0,0 +1,56 @@ +from pathlib import Path + +from fastapi import HTTPException +from pydantic import BaseModel + +from nopywer.api import OptimizeRequest +from nopywer.io import load_geojson +from nopywer.models import PowerGrid + +# from nopywer.optimize import optimize_layout +# from nopywer.ortools_solverV1 import optimize_layout +from nopywer.optimize_mixed import optimize_layout + +class OptimizeResponse(BaseModel): + cables_geojson: dict + total_cable_length_m: float + num_cables: int + phase_loads: dict[int, float] + + +def optimize(req: OptimizeRequest): + nodes, _ = load_geojson(req.nodes_geojson) + if not nodes: + raise HTTPException(400, "No valid nodes found in GeoJSON") + + try: + grid = PowerGrid(nodes=nodes, cables={}) + except ValueError as exc: + raise HTTPException(400, str(exc)) + + loads = [n for n in grid.nodes.values() if not n.is_generator] + if not loads: + raise HTTPException(400, "At least one load is required") + + grid = optimize_layout(grid, extra_cable_m=req.extra_cable_m, + hub_discount=0.7, radiality_factor=0.3) + + return OptimizeResponse( + cables_geojson=grid.to_geojson(), + total_cable_length_m=round( + sum(c.length_m for c in grid.cables.values()), + 1, + ), + num_cables=len(grid.cables), + phase_loads=grid.phase_loads, + ) + + +if __name__ == "__main__": + import json + + with open(Path(__file__).parent.parent.parent / "tests/fixtures/export.geojson") as f: + nodes_geojson = json.load(f) + req = OptimizeRequest(nodes_geojson=nodes_geojson) + res = optimize(req) + print(json.dumps(res.dict(), indent=2)) diff --git a/src/nopywer/frontend/app.js b/src/nopywer/frontend/app.js index 60039a4..36685f4 100644 --- a/src/nopywer/frontend/app.js +++ b/src/nopywer/frontend/app.js @@ -23,14 +23,26 @@ const nodesLayer = L.layerGroup().addTo(map); const optimizedCablesLayer = L.layerGroup().addTo(map); + const nodeMarkers = []; + let highlightedMarkers = []; + let nodesGeojson = null; let defaultBounds = null; - function setMessage(text, isError) { - messageEl.textContent = text; - messageEl.style.color = isError ? "#fca5a5" : "#cbd5e1"; + // function setMessage(text, isError) { + // messageEl.textContent = text; + // messageEl.style.color = isError ? "#fca5a5" : "#cbd5e1"; + // } + function setMessage(html, isError) { + messageEl.innerHTML = html; // ✅ allows
and + messageEl.style.color = isError ? "#fca5a5" : "#cbd5e1"; } + + function capitalizeName(name) { + return name.replace(/\b\w/g, c => c.toUpperCase()); + } + function setOptimizerStatus(text) { optimizerStatusEl.textContent = text; } @@ -43,6 +55,113 @@ }); } + function clearNodeHighlight() { + highlightedMarkers.forEach((marker) => { + if (marker._originalStyle) { + marker.setStyle(marker._originalStyle); + } + }); + highlightedMarkers = []; + } + + function highlightNodeMarkers(markers) { + clearNodeHighlight(); + if (!markers.length) { + return; + } + + const bounds = L.latLngBounds(markers.map((marker) => marker.getLatLng())); + markers.forEach((marker) => { + marker.setStyle({ + radius: Math.max(marker.options.radius, marker.options.radius * 1.5), + weight: Math.max(marker.options.weight, 4), + color: "#2563eb", + fillColor: "#60a5fa", + fillOpacity: 1, + opacity: 1, + }); + highlightedMarkers.push(marker); + }); + + map.fitBounds(bounds.pad(0.15)); + markers[0].openPopup(); + } + + function searchNodeByName() { + const input = document.getElementById("node-search-input"); + if (!input) { + return; + } + + const query = input.value.trim().toLowerCase(); + clearNodeHighlight(); + + if (!query) { + setMessage("Enter a node name to find it on the map.", false); + return; + } + + const matches = nodeMarkers + .filter((entry) => entry.name.toLowerCase().includes(query)) + .map((entry) => entry.layer); + + if (!matches.length) { + setMessage('No nodes found matching "' + input.value + '".', true); + return; + } + + setMessage('Found ' + matches.length + ' node' + (matches.length === 1 ? '' : 's') + '.', false); + highlightNodeMarkers(matches); + } + + const SearchControl = L.Control.extend({ + onAdd: function () { + const container = L.DomUtil.create("div", "leaflet-bar leaflet-control leaflet-control-custom"); + container.style.background = "#ffffffee"; + container.style.padding = "6px 8px"; + container.style.borderRadius = "4px"; + container.style.boxShadow = "0 1px 4px rgba(0,0,0,0.3)"; + container.style.minWidth = "220px"; + container.style.fontFamily = "sans-serif"; + container.style.fontSize = "13px"; + + const input = L.DomUtil.create("input", "", container); + input.type = "search"; + input.placeholder = "Search node name..."; + input.style.width = "100%"; + input.style.marginBottom = "5px"; + input.style.padding = "4px 6px"; + input.style.border = "1px solid #ccc"; + input.style.borderRadius = "3px"; + input.id = "node-search-input"; + + const button = L.DomUtil.create("button", "", container); + button.type = "button"; + button.textContent = "Find"; + button.style.width = "100%"; + button.style.padding = "4px 6px"; + button.style.border = "1px solid #666"; + button.style.borderRadius = "3px"; + button.style.background = "#374151"; + button.style.color = "#f8fafc"; + button.style.cursor = "pointer"; + button.id = "node-search-button"; + + L.DomEvent.disableClickPropagation(container); + L.DomEvent.on(button, "click", searchNodeByName); + L.DomEvent.on(input, "keydown", function (evt) { + if (evt.key === "Enter") { + evt.preventDefault(); + searchNodeByName(); + } + }); + + return container; + }, + }); + + map.addControl(new SearchControl({ position: "topright" })); + function isGenerator(feature) { const name = String(feature.properties && feature.properties.name ? feature.properties.name : ""); return name.toLowerCase().includes("generator"); @@ -65,25 +184,38 @@ const generator = isGenerator(feature); const name = feature.properties && feature.properties.name ? feature.properties.name : "unnamed"; const power = feature.properties && typeof feature.properties.power === "number" - ? feature.properties.power / 1000 - : 0; - - L.circleMarker(latlng, { - radius: generator ? 8 : 6, + ? feature.properties.power / 1000 + : 0; + + const phaseRaw = feature.properties?.phase; + const phase = phaseRaw != null && phaseRaw !== "" + ? parseInt(phaseRaw, 10) + : null; + + const phaseLabel = phase !== null && !isNaN(phase) ? `L${phase + 1}` : "No phase assigned"; + const phaseCol = phaseColor(phase); + // Scale radius proportionally to power (min 4, max 20) + const radius = Math.max(4, Math.min(30, 4 + power * 1)); + const markerOptions = { + radius: generator ? Math.max(radius, 8) : radius, weight: generator ? 3 : 2, - color: generator ? "#ef4444" : "#a16207", - fillColor: generator ? "#f87171" : "#facc15", + color: "#eecb6b", + fillColor: generator ? "#f87171" : phaseCol, + // fillColor: generator ? "#f87171" : "#facc15", fillOpacity: 0.95, - }) - .bindPopup( - "" + - name + - "
" + - "Power: " + - power.toFixed(2) + - " kW", - ) + opacity: 1, + }; + + const marker = L.circleMarker(latlng, markerOptions) + .bindPopup(` + ${capitalizeName(name)}
+ Power: ${power.toFixed(2)} kW
+ Phase: ${phaseLabel} + `) .addTo(nodesLayer); + marker._originalStyle = markerOptions; + marker._nodeName = name; + nodeMarkers.push({ name, layer: marker }); bounds.push(latlng); }); @@ -100,67 +232,132 @@ const plugs = feature.properties && feature.properties.plugs_and_sockets_a ? feature.properties.plugs_and_sockets_a : 16; + const phase = feature.properties?.phase ?? null; + const phaseCol = phaseColor(phase); if (plugs >= 63) { - return { color: "#dc2626", weight: 6 }; + return {color: phaseCol, weight: 10 }; } if (plugs >= 32) { - return { color: "#ea580c", weight: 5 }; + return { color: phaseCol, weight: 7 }; } - return { color: "#f59e0b", weight: 4 }; + return { color: phaseCol, weight: 3 }; + } + + function getCableWeight(plugs) { + if (plugs >= 63) return 8; + if (plugs >= 32) return 6; + return 4; + } + // function getCableColor(plugs) { + // if (plugs >= 63) return "#dc2626"; + // if (plugs >= 32) return "#ea580c"; + // return "#f59e0b"; + // } + + function phaseColor(phase) { + const p = phase !== null && phase !== "" ? parseInt(phase, 10) : null; + const colors = { 0: "#000000dc", 1: "#77261c", 2: "#7a7a7a", 3: "#ff00a6" }; + return p !== null && !isNaN(p) ? (colors[p] ?? "#f8f7f7") : "#facc15"; } function renderOptimizedCables(cablesGeojson) { - optimizedCablesLayer.clearLayers(); + console.log(cablesGeojson.features[0].properties); + + optimizedCablesLayer.clearLayers(); - const cableLayer = L.geoJSON(cablesGeojson, { - filter: function (feature) { - return feature.geometry && feature.geometry.type === "LineString"; - }, - style: function (feature) { - const style = cableStyle(feature); + const filter = function (feature) { + return feature.geometry && feature.geometry.type === "LineString"; + }; + + const cableLayer = L.geoJSON(cablesGeojson, { + filter, + style: function (feature) { + const style = cableStyle(feature); return { color: style.color, weight: style.weight, - opacity: 0.95, + opacity: 1, }; - }, - onEachFeature: function (feature, layer) { - const props = feature.properties || {}; - const details = - "" + - (props.from || "?") + - " → " + - (props.to || "?") + - "
" + - "Length: " + - String(props.length_m ?? "?") + - " m
" + - "Cable: " + - String(props.cable_type || "?") + - "
" + - "Current: " + - String(props.current_a ?? "?") + - " A
" + - "Load: " + - String(props.cum_power_kw ?? "?") + - " kW"; - - layer.bindPopup(details); - layer.bindTooltip(details, { - sticky: true, - direction: "top", - className: "cable-tooltip", - }); - }, - }); + }, + onEachFeature: function (feature, layer) { + const props = feature.properties || {}; + const phaseRaw = props.phase ?? null; + const phase = phaseRaw !== null && phaseRaw !== "" ? parseInt(phaseRaw, 10) : null; + const plugs = props.plugs_and_sockets_a ?? 16; + + const vdropStr = props.vdrop_pct !== null && props.vdrop_pct !== undefined + ? (() => { + const vdrop = props.vdrop_pct; + const vdropColor = vdrop > 5 ? "#e74c3c" : vdrop > 3 ? "#f39c12" : "#2ecc71"; + return `▼ ${vdrop.toFixed(1)}%`; + })() + : ""; + + + const phaseLabel = + phase === null || isNaN(phase) ? "?" + : phase === 3 ? "Triphasic" + : "L" + (phase + 1); + + layer.bindPopup( + `${capitalizeName(props.from || "?")} → ${capitalizeName(props.to || "?")}
+ Phase: ${phaseLabel}
+ Cable: ${plugs}A
+ Length: ${props.length_m ?? "?"} m
+ Current: ${props.current_a ?? "?"} A
+ Load: ${props.cum_power_kw ?? "?"} kW` + ); + }, +}); + +cableLayer.addTo(optimizedCablesLayer); + bringNodesToFront(); +} +function updateNodePhasesFromResponse(nodeFeatures) { + nodesLayer.clearLayers(); + nodeMarkers.length = 0; + const seen = new Set(); + + nodeFeatures.forEach((feature) => { + if (!feature.geometry || feature.geometry.type !== "Point") return; + + const coords = feature.geometry.coordinates; + const latlng = [coords[1], coords[0]]; + + const name = feature.properties?.name ?? "unnamed"; + if (seen.has(name)) { + console.warn("Duplicate node skipped:", name); + return; + } + seen.add(name); + + const phase = feature.properties?.phase ?? null; + const power = (feature.properties?.power_watts ?? 0) / 1000; + const nodeColor = phaseColor(phase); + + const phaseLabel = phase !== null && !isNaN(phase) ? `L${phase + 1}` : "No phase"; + const generator = name.toLowerCase().includes("generator"); + const radius = Math.max(4, Math.min(20, 4 + power)); + + const markerOptions = { + radius: generator ? Math.max(radius, 8) : radius, + weight: 3, + color: "#eecb6b", + fillColor: nodeColor, + fillOpacity: 0.95, + opacity: 1, + }; - cableLayer.addTo(optimizedCablesLayer); - if (cableLayer.getLayers().length > 0) { - map.fitBounds(cableLayer.getBounds().pad(0.08)); - } - bringNodesToFront(); - } + const marker = L.circleMarker(latlng, markerOptions) + .bindPopup(`${capitalizeName(name)}
Power: ${power.toFixed(2)} kW
Phase: ${phaseLabel}`) + .addTo(nodesLayer); + + marker._originalStyle = markerOptions; + nodeMarkers.push({ name, layer: marker }); + }); + +} function buildNodesGeojson() { if (!nodesGeojson || !Array.isArray(nodesGeojson.features)) { @@ -175,28 +372,58 @@ properties: { name: feature.properties.name, power: feature.properties.power, + phase: feature.properties.phase ?? null, }, })), }; } - async function loadNodes() { - const response = await fetch(DATASET_URL); - if (!response.ok) { - throw new Error("Bundled power nodes dataset is unavailable"); + async function loadNodes(file = null) { + if (file) { + const text = await file.text(); + nodesGeojson = JSON.parse(text); + setMessage("Loaded custom dataset.", false); + } else { + const response = await fetch(DATASET_URL); + if (!response.ok) { + throw new Error("Bundled power nodes dataset is unavailable"); + } + nodesGeojson = await response.json(); + setMessage("Loaded the minimal power-only dataset bundled with nopywer.", false); } - - nodesGeojson = await response.json(); const features = Array.isArray(nodesGeojson.features) ? nodesGeojson.features : []; nodeCountEl.textContent = String(features.length); addNodes(features); - setMessage("Loaded the minimal power-only dataset bundled with nopywer.", false); } + const uploadButton = document.getElementById("upload-button"); + const uploadInput = document.getElementById("upload-input"); + + uploadButton.addEventListener("click", () => uploadInput.click()); + + uploadInput.addEventListener("change", function () { + const file = this.files?.[0]; + if (!file) return; + + // Reset state before loading new nodes + optimizedCablesLayer.clearLayers(); + nodesLayer.clearLayers(); + nodeMarkers.length = 0; + setOptimizerStatus("idle"); + loadNodes(file).catch((error) => { + nodeCountEl.textContent = "0"; + setOptimizerStatus("error"); + setMessage(escapeHtml(error.message) || "Failed to load file", true); + }); + + this.value = ""; // ✅ allows re-uploading the same file + }); async function runOptimization() { optimizeButton.disabled = true; setOptimizerStatus("running"); setMessage("Optimizing the current bundled power nodes dataset...", false); + const hubDiscount = parseFloat(document.getElementById("hub-discount").value); + const radialityFactor = parseFloat(document.getElementById("radiality-factor").value); try { const response = await fetch(OPTIMIZE_URL, { @@ -207,6 +434,8 @@ body: JSON.stringify({ nodes_geojson: buildNodesGeojson(), extra_cable_m: 10, + hub_discount: hubDiscount, + radiality_factor: radialityFactor, }), }); @@ -216,19 +445,26 @@ } renderOptimizedCables(payload.cables_geojson); + updateNodePhasesFromResponse( + payload.cables_geojson.features.filter(f => f.geometry.type === "Point")); setOptimizerStatus("done"); + bringNodesToFront(); setMessage( - "Computed and drew " + - payload.num_cables + - " cables for " + - Math.round(payload.total_cable_length_m) + - " m total.", - false, + `Computed and drew ${payload.num_cables} cables for ${Math.round(payload.total_cable_length_m)} m total — ` + + `Phase loads: L1=${Math.round(payload.phase_loads[0])} kW, ` + + `L2=${Math.round(payload.phase_loads[1])} kW, ` + + `L3=${Math.round(payload.phase_loads[2])} kW`, + false ); - } catch (error) { + + } catch (error) { setOptimizerStatus("error"); - setMessage(error.message || "Optimization failed", true); - } finally { + const line = error.stack + ? `
${escapeHtml(error.stack.split("\n")[1]?.trim() ?? "")}` + : ""; + setMessage((escapeHtml(error.message) || "Optimization failed") + line, true); + } + finally { optimizeButton.disabled = false; } } diff --git a/src/nopywer/frontend/data/power-nodes.geojson b/src/nopywer/frontend/data/power-nodes.geojson index 2328905..61483f9 100644 --- a/src/nopywer/frontend/data/power-nodes.geojson +++ b/src/nopywer/frontend/data/power-nodes.geojson @@ -319,8 +319,8 @@ ] }, "properties": { - "name": "Gloria Garden", - "power": 200.0 + "name": "distro example", + "power": 0.0 } }, { @@ -504,216 +504,6 @@ "name": "Dreamdust Field", "power": 3680.0 } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011537103633567, - 41.69234806304274 - ] - }, - "properties": { - "name": "Practice Patch", - "power": 0.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011918685307272, - 41.69233028850832 - ] - }, - "properties": { - "name": "Ninth Lantern Well", - "power": 0.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011454117307237, - 41.69206124659373 - ] - }, - "properties": { - "name": "Roundbelly Row", - "power": 1750.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.01038655752624, - 41.69173572377932 - ] - }, - "properties": { - "name": "Question Trail", - "power": 1750.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.010501165300565, - 41.69218235691341 - ] - }, - "properties": { - "name": "Tranquil Grove", - "power": 1750.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.010766203068702, - 41.69146748741722 - ] - }, - "properties": { - "name": "Wonder Hollow", - "power": 1750.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011262077140324, - 41.69148675626195 - ] - }, - "properties": { - "name": "Playground Glade", - "power": 1750.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011383061767252, - 41.69185487542429 - ] - }, - "properties": { - "name": "Octopus Garden", - "power": 0.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.010967332089935, - 41.69193901038799 - ] - }, - "properties": { - "name": "Pebble Depot Six", - "power": 0.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.010941211222839, - 41.69189210024385 - ] - }, - "properties": { - "name": "Lantern House Six", - "power": 1000.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011254531115862, - 41.69252267281408 - ] - }, - "properties": { - "name": "Second Pump House", - "power": 1000.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011064596212925, - 41.69292156377749 - ] - }, - "properties": { - "name": "Glimmer Glen", - "power": 1750.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.010981512031082, - 41.69253687244502 - ] - }, - "properties": { - "name": "Curious Hollow", - "power": 8800.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011275265114728, - 41.69248508605406 - ] - }, - "properties": { - "name": "Fourth Pump House", - "power": 1000.0 - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 0.011861951718222, - 41.69255180775979 - ] - }, - "properties": { - "name": "Tenth Pump House", - "power": 1000.0 - } } ] -} +} \ No newline at end of file diff --git a/src/nopywer/frontend/favicon.ico b/src/nopywer/frontend/favicon.ico new file mode 100644 index 0000000..0e5b65f Binary files /dev/null and b/src/nopywer/frontend/favicon.ico differ diff --git a/src/nopywer/frontend/index.html b/src/nopywer/frontend/index.html index 847279f..e42f13a 100644 --- a/src/nopywer/frontend/index.html +++ b/src/nopywer/frontend/index.html @@ -20,7 +20,20 @@

Frontend for development only

Minimal embedded map for nopywer.

+ + +
+ + +
+
+ + + +
diff --git a/src/nopywer/inventory.py b/src/nopywer/inventory.py index f89887c..08a6549 100644 --- a/src/nopywer/inventory.py +++ b/src/nopywer/inventory.py @@ -27,7 +27,6 @@ def find_combinations(arr: list, target_sum, th: float = 5.0): def choose_cables(inventory_file: str, cables_dict: dict) -> None: - verbose = 1 unmatched = [] logger.info(f"\nReading cables inventory from {inventory_file}") df = pd.read_excel( @@ -36,23 +35,11 @@ def choose_cables(inventory_file: str, cables_dict: dict) -> None: skiprows=0, ) - if verbose >= 3: - logger.debug(f"\t {df}") - sorted_cables = sorted(cables_dict.values(), key=lambda c: c.length_m, reverse=True) for idx, cable in enumerate(sorted_cables): - if verbose >= 2: - logger.debug( - f"\n\t\t\t taking care of cable {idx + 1}/{len(sorted_cables)}, " - f"length {cable.length_m} m" - ) - n_phases = 3 if cable.plugs_and_sockets_a > 16 else 1 - if verbose >= 2: - logger.debug(f" n phases: {n_phases}") - compatible_rows = ( (df["number of phases"] == n_phases) & (df["plugs&sockets [A]"] == cable.plugs_and_sockets_a) @@ -60,13 +47,10 @@ def choose_cables(inventory_file: str, cables_dict: dict) -> None: ) compatible_df = df[compatible_rows] - if verbose >= 2: - logger.debug(f"\t\t\t compatible cables dataframe: \n {compatible_df}") - comb = None if compatible_df.empty: - if verbose >= 2: - logger.debug("DataFrame is empty --> no compatible cables in inventory!") + pass + else: list_of_cables = [ length @@ -76,18 +60,13 @@ def choose_cables(inventory_file: str, cables_dict: dict) -> None: target_sum = cable.length_m comb = find_combinations(list_of_cables, target_sum) - if comb is None or verbose: + if comb is None: nodes = [cable.from_node, cable.to_node] - logger.debug(f"\t\t\t cable {nodes}: {comb}") - if comb is not None: for c in comb: - df.loc[compatible_rows & (df["length [m]"] == c), "quantity"] -= 1 - if verbose >= 2: - remaining = df.loc[ - compatible_rows & (df["length [m]"] == c), "quantity" - ].values - logger.debug(f"\t\t\t qty of {c}m remaining: {remaining}") + df.loc[compatible_rows & (df["length [m]"] == c), "quantity"] -= ( + 1 # One cable is used + ) if comb is None: unmatched.append(cable) @@ -99,7 +78,7 @@ def choose_cables(inventory_file: str, cables_dict: dict) -> None: f"\t {unm.plugs_and_sockets_a}{'A':.<4} ({unm.length_m:.0f}m) {'-'.join(nodes)} " ) - return None + return unmatched def parse_distro_req(req: str) -> tuple[str, float]: diff --git a/src/nopywer/models.py b/src/nopywer/models.py index 17006ca..0ea47e0 100644 --- a/src/nopywer/models.py +++ b/src/nopywer/models.py @@ -4,7 +4,7 @@ import numpy as np -from .constants import PF, RHO_COPPER, V0 +from .constants import PF, RHO_COPPER, T, T0, V0 @dataclass @@ -14,7 +14,7 @@ class PowerNode: lat: float power_watts: float = 0.0 is_generator: bool = False - phase: int | str | list | None = None + phase: int = 0 parent: str | None = None children: dict[str, str] = field(default_factory=dict) @@ -56,6 +56,7 @@ def to_geojson(self) -> dict: "vdrop_percent": self.vdrop_percent, "i_sc_ka": self.i_sc_ka, "distro": self.distro, + "phase": self.phase, }, } @@ -66,7 +67,7 @@ class Cable: length_m: float area_mm2: float = 2.5 plugs_and_sockets_a: float = 16.0 - phase: int | str | list | None = None + phase: int = 0 from_node: str = "" to_node: str = "" @@ -91,8 +92,10 @@ def __setattr__(self, name, value): @property def resistance(self) -> float: - return RHO_COPPER * self.length_m / self.area_mm2 - + # return RHO_COPPER * self.length_m / self.area_mm2 + return RHO_COPPER*(1+0.0393*(T-T0)) * self.length_m / self.area_mm2 #Temperature correction for 50°C, see https://en.wikipedia.org/wiki/Electrical_resistivity_and_conductivity#Temperature_dependence + + def to_geojson(self) -> dict: max_current = max(self.current_per_phase) if self.current_per_phase else 0.0 cum_power_w = max_current * V0 * PF * type(self).num_phases @@ -117,6 +120,7 @@ def to_geojson(self) -> dict: "current_a": round(max_current, 1), "cum_power_kw": round(cum_power_w / 1000, 2), "vdrop_volts": self.vdrop_volts, + "phase": self.phase, }, } @@ -128,6 +132,7 @@ class Cable16A(Cable): tier_cost: ClassVar[float] = 1.0 num_phases: ClassVar[int] = 1 max_current_a: ClassVar[int] = 16 + area_mm2: float = 2.5 @dataclass @@ -144,7 +149,7 @@ class Cable63A(Cable): tier_cost: ClassVar[float] = 8.0 num_phases: ClassVar[int] = 3 max_current_a: ClassVar[int] = 63 - area_mm2: float = 16.0 + area_mm2: float = 16.0 plugs_and_sockets_a: float = 63.0 @@ -162,7 +167,6 @@ class Cable125A(Cable): def pick_cable_for(power_watts: float) -> type[Cable]: """Pick the smallest cable type that can handle the given power. - Each type is checked with: I = P / (num_phases x V0 x PF) """ for cable_cls in _CABLE_TYPES: diff --git a/src/nopywer/optimization_tools.py b/src/nopywer/optimization_tools.py new file mode 100644 index 0000000..c21777b --- /dev/null +++ b/src/nopywer/optimization_tools.py @@ -0,0 +1,272 @@ +import logging + +import openpyxl + +from nopywer.models import _CABLE_TYPES +MULTI_PHASE_THRESHOLD_W = 10000 # 10kW + +MULTI_PHASE_3_THRESHOLD_W = 11000 # >= 10kW → 3 phases +MULTI_PHASE_2_THRESHOLD_W = 8000 # >= 5kW → 2 phases + # < 5kW → 1 phases + + +def assign_phases_from_grid(nodes: dict, cables: list, logger: logging.Logger) -> tuple: + children = {n: [] for n in nodes} + cable_map = {} + for cable in cables: + children[cable.from_node].append(cable.to_node) + cable_map[(cable.from_node, cable.to_node)] = cable + + generator = next(n for n, node in nodes.items() if node.is_generator) + load_cache = {} + descendant_cache = {} + + def get_load(root): + if root in load_cache: + return load_cache[root] + total, stack = 0, [root] + while stack: + n = stack.pop() + total += nodes[n].power_watts + stack.extend(children[n]) + load_cache[root] = total + return total + + def get_descendants(root): + if root in descendant_cache: + return descendant_cache[root] + count, stack = 0, list(children[root]) + while stack: + n = stack.pop() + count += 1 + stack.extend(children[n]) + descendant_cache[root] = count + return count + + for n in nodes: + get_load(n) + get_descendants(n) + + total_load = sum(n.power_watts for n in nodes.values() if not n.is_generator) + total_nodes = len(nodes) - 1 + THREE_PHASE_DESCENDANT_THRESHOLD = total_nodes // 3 + THREE_PHASE_LOAD_THRESHOLD = total_load // 3 + + def is_distro(name: str) -> bool: + return name.lower().startswith('distro') + + def should_receive_three_phase(from_node, to_node) -> bool: + cable = cable_map.get((from_node, to_node)) + num_phases = getattr(cable, 'num_phases', 3) if cable else 3 + if num_phases < 3: + return False + descendants = get_descendants(to_node) + load = get_load(to_node) + return ( + descendants >= THREE_PHASE_DESCENDANT_THRESHOLD or + load >= THREE_PHASE_LOAD_THRESHOLD or + is_distro(to_node) # distros always receive 3-phase if possible + ) + + + # Almacena el reparto real de cada nodo: {node: {0: W, 1: W, 2: W}} + node_phase_split = {} + + def split_load_across_phases(node, load_w, phase_loads) -> dict: + """ + Decides how to split node load across phases based on power. + Returns {phase: watts} and updates phase_loads in place. + """ + if load_w >= MULTI_PHASE_3_THRESHOLD_W: + # 3 phases: equitable distribution + split = {p: load_w / 3 for p in range(3)} + + elif load_w >= MULTI_PHASE_2_THRESHOLD_W: + # 2 phases: the two least loaded + sorted_phases = sorted(phase_loads, key=lambda p: phase_loads[p]) + p1, p2 = sorted_phases[0], sorted_phases[1] + split = {p1: load_w / 2, p2: load_w / 2} + + else: #only one phase + assigned = min(phase_loads, key=lambda p: phase_loads[p]) + split = {assigned: load_w} + + # Global counter update + for p, w in split.items(): + phase_loads[p] += w + + logger.info( + f"[PHASE SPLIT] {node}: {load_w:.0f}W → " + + ", ".join(f"L{p}={w:.0f}W" for p, w in split.items()) + ) + return split + + def assign_phases_to_kids_balanced(node, kids, phase_loads): + """ + Greedy balanced assignment for kids of a 3-phase node. + Looks ahead at full subtree load of each kid. + Returns {kid: phase} assignments. + """ + assignments = {} + local_loads = phase_loads.copy() + + for kid in sorted(kids, key=get_load, reverse=True): + kid_load = get_load(kid) + cable = cable_map.get((node, kid)) + num_phases = getattr(cable, 'num_phases', 3) if cable else 3 + + if should_receive_three_phase(node, kid) or (kid_load >= MULTI_PHASE_2_THRESHOLD_W and num_phases == 3): + assignments[kid] = 3 # cable trifásico + # Split de carga según potencia + split = split_load_across_phases(kid, kid_load, local_loads) + node_phase_split[kid] = split + else: + assigned = min(local_loads, key=lambda p: local_loads[p]) + local_loads[assigned] += kid_load + assignments[kid] = assigned + node_phase_split[kid] = {assigned: kid_load} + + # Sync local_loads back to phase_loads + for p in phase_loads: + phase_loads[p] = local_loads[p] + + return assignments + + node_phase = {generator: 3} + cable_phase = {} + phase_loads = {0: 0.0, 1: 0.0, 2: 0.0} + + queue = [(generator, 3)] + + while queue: + node, incoming_phase = queue.pop(0) + kids = sorted(children[node], key=get_load, reverse=True) + + if not kids: + continue + + if incoming_phase == 3: + assignments = assign_phases_to_kids_balanced(node, kids, phase_loads) + + for kid, phase in assignments.items(): + cable_phase[(node, kid)] = phase + node_phase[kid] = phase + queue.append((kid, phase)) + + else: + # Single-phase: check if kid is high-power enough to warrant 3-phase upgrade + for kid in kids: + kid_load = get_load(kid) + cable = cable_map.get((node, kid)) + num_phases = getattr(cable, 'num_phases', 3) if cable else 3 + + if kid_load >= MULTI_PHASE_THRESHOLD_W and num_phases == 3: + # High power node on single-phase branch: upgrade to 3-phase + cable_phase[(node, kid)] = 3 + node_phase[kid] = 3 + for p in range(3): + phase_loads[p] += kid_load / 3 + queue.append((kid, 3)) + logger.info( + f"[PHASE] {kid}: upgraded to 3-phase " + f"(load={kid_load:.0f}W >= {MULTI_PHASE_THRESHOLD_W}W)" + ) + else: + cable_phase[(node, kid)] = incoming_phase + node_phase[kid] = incoming_phase + queue.append((kid, incoming_phase)) + + # Balance report + real_phase_loads = {0: 0.0, 1: 0.0, 2: 0.0} + for n, split in node_phase_split.items(): + if n.startswith('distro'): + continue + for p, w in split.items(): + real_phase_loads[p] += w + + total = sum(real_phase_loads.values()) + for p, load in real_phase_loads.items(): + pct = 100 * load / total if total else 0 + logger.info(f"[PHASE] L{p}: {load:.0f}W ({pct:.1f}%)") + if total: + imbalance = max(real_phase_loads.values()) - min(real_phase_loads.values()) + logger.info(f"[PHASE] imbalance: {imbalance:.0f}W ({100*imbalance/total:.1f}%)") + + return node_phase, cable_phase, real_phase_loads + + +def segments_cross(p1, p2, p3, p4): + """Returns True if segment p1-p2 crosses p3-p4""" + def cross(o, a, b): + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) + + def on_segment(p, q, r): + return min(p[0], r[0]) <= q[0] <= max(p[0], r[0]) and min(p[1], r[1]) <= q[1] <= max( + p[1], r[1] + ) + d1 = cross(p3, p4, p1) + d2 = cross(p3, p4, p2) + d3 = cross(p1, p2, p3) + d4 = cross(p1, p2, p4) + + if ((d1 > 0 and d2 < 0) or (d1 < 0 and d2 > 0)) and ( + (d3 > 0 and d4 < 0) or (d3 < 0 and d4 > 0) + ): + return True + if d1 == 0 and on_segment(p3, p1, p4): + return True + if d2 == 0 and on_segment(p3, p2, p4): + return True + if d3 == 0 and on_segment(p1, p3, p2): + return True + if d4 == 0 and on_segment(p1, p4, p2): + return True + return False + + +def load_inventory(path: str) -> list[dict]: + wb = openpyxl.load_workbook(path) + ws = wb.active + inventory = [] + for row in ws.iter_rows(min_row=2, values_only=True): + phases, section, plugs, length, quantity = row + for i in range(int(quantity)): + inventory.append( + { + "id": f"cable_{section}mm2_{plugs}A_{length}m_{i}", + "section_mm2": float(section), + "plugs_a": float(plugs), + "length_m": float(length), + "num_phases": int(phases), + "tier": next( + t for t, tier in enumerate(_CABLE_TYPES) if tier.area_mm2 == float(section) + ), + } + ) + return inventory + + +# def phase_assignment_greedy(grid: dict): +# """ +# each item has the foloowing: +# - 'power' (or 'cum_power') +# - name +# - .... +# """ +# phases = [{"total_load": 0}, {"total_load": 0}, {"total_load": 0}] + +# loads_unsorted = {key: value["power"].sum() for key, value in grid.items()} +# loads = dict(sorted(loads_unsorted.items(), key=lambda x: x[1], reverse=True)) +# for key, value in loads.items(): +# assigned_phase = min(range(len(phases)), key=lambda i: phases[i]["total_load"]) +# # grid[key]['assigned_phase'] = assigned_phase +# phases[assigned_phase]["total_load"] += value +# print(f"{key}: {value:.0f}W, phase {assigned_phase}") + +# print(f"\ntotal on phases: {phases}") +# phase_balance = 100 * np.std( +# grid["generator"]["cum_power"] / np.mean(grid["generator"]["cum_power"]) +# ) +# print(f"balance : {phase_balance:.1f}%") + +# return loads diff --git a/src/nopywer/optimize_mixed.py b/src/nopywer/optimize_mixed.py new file mode 100644 index 0000000..ffbce88 --- /dev/null +++ b/src/nopywer/optimize_mixed.py @@ -0,0 +1,379 @@ +import math +import networkx as nx +import itertools +import logging +import os + + +from nopywer.analyze import _compute_distro_requirements +from nopywer.constants import EXTRA_CABLE_LENGTH_M, PF, RHO_COPPER, V0, VDROP_THRESHOLD_PERCENT +from nopywer.geometry import geodesic_distance_m +from nopywer.models import _CABLE_TYPES, Cable, PowerGrid +from nopywer.optimization_tools import assign_phases_from_grid +from nopywer.optimize import _compute_power_flow + +logger = logging.getLogger(__name__) +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + datefmt="%m-%d %H:%M", + filename="optimizer.log", + filemode="w", +) +console = logging.StreamHandler() +console.setLevel(logging.INFO) +formatter = logging.Formatter("%(name)-12s: %(levelname)-8s %(message)s") +console.setFormatter(formatter) +logging.getLogger().addHandler(console) + + +MAX_POWER = 100000 +RADIUS_M = 50 # tune this: typical cluster radius in your layout + +INVENTORY = os.getenv("NOPYWER_INVENTORY", "orange_inventory.xlsx") + +# basically, for any grid generated by the optim algo: +# you need to compute the voltage drop, current in each cables etc. +# So phases need to be assigned first. Then you check if the grid is valid +# then, only for those feasible solutions, you try to optimize voltage drop, total length, etc + +def optimize_layout(grid: PowerGrid, extra_cable_m=EXTRA_CABLE_LENGTH_M, + hub_discount=0.7, radiality_factor=0.3) -> PowerGrid: + # 0 = no discount, 1 = free connection to perfect hub + # Radiality factor: penalizes edges between non-generator nodes + # Higher RADIALITY_FACTOR = more star-shaped (all connect to generator) + # Lower RADIALITY_FACTOR = more chained (MST-like) + + nodes = grid.nodes + node_names = list(nodes.keys()) + generator = next(n for n, node in nodes.items() if node.is_generator) + vdrop = None + + G = nx.Graph() + max_section = max(tier.area_mm2 for tier in _CABLE_TYPES) + + dist_matrix = {} + define_distance_matrix(nodes, node_names, dist_matrix) + + # Precompute hub score for each node: + # how much load is reachable within a radius from this node + hub_score = precompute_hub(nodes, node_names, dist_matrix) + + max_hub = max(hub_score.values()) if hub_score else 1 + generator_position = (nodes[generator].lon, nodes[generator].lat) + + logger.info(f"[HUB] scores: {sorted(hub_score.items(), key=lambda x: x[1], reverse=True)[:5]}") + for i, j in itertools.combinations(node_names, 2): + d = dist_matrix[(i, j)] + base_weight = RHO_COPPER / max_section * d * d + + if nodes[i].is_generator or nodes[j].is_generator: + hub_node = j if nodes[i].is_generator else i + + if is_distro(hub_node): + # hub_discount: cuanto mayor, más barato conectar distros al generador + weight = base_weight * (1 - hub_discount) * 0.1 + else: + # radiality_factor: cuanto mayor, más caro conectar non-distro al generador + weight = base_weight * (1 + radiality_factor * 10) + + elif is_distro(i) and is_distro(j): + # distro↔distro: cheap only if closer to each other than to generator + d_i_gen = dist_matrix[(i, generator)] + d_j_gen = dist_matrix[(j, generator)] + if d < min(d_i_gen, d_j_gen): + weight = base_weight * (1 - hub_discount) * 0.1 + else: + weight = base_weight * (1 + radiality_factor) + + elif is_distro(i) or is_distro(j): + # non-distro↔distro: cheap + weight = base_weight * (1 - hub_discount * 0.8) + + else: + # non-distro↔non-distro: expensive + weight = base_weight * (1 + radiality_factor * 5) + + G.add_edge(i, j, weight=weight) + + mst = nx.minimum_spanning_tree(G, algorithm='kruskal') + if generator not in mst: + logger.warning("Generator %s is not present in MST nodes, using fallback root", generator) + generator = next(i for i in mst.nodes if 'generator' in str(i).lower()) + + oriented_tree = nx.bfs_tree(mst, source=generator) + for node in oriented_tree.nodes: + if not is_distro(node) or nodes[node].is_generator: + continue + parent = next(iter(oriented_tree.predecessors(node)), None) + if parent and not nodes[parent].is_generator and not is_distro(parent): + logger.warning( + f"[DISTRO] {node} is connected to non-distro {parent} — " + f"constraint not satisfied, increase distro↔non-distro penalty" + ) + + tiers = assign_tiers_to_nodes(oriented_tree, nodes, dist_matrix) + + # vdrop, warnings = compute_voltage_drop(oriented_tree, nodes, dist_matrix, tiers) + + # if warnings: + # oriented_tree = fix_voltage_drop_warnings(oriented_tree, nodes, dist_matrix, warnings, tiers) + # # Recompute tiers and vdrop after fix + # tiers = assign_tiers_to_nodes(oriented_tree, nodes) + # vdrop, warnings = compute_voltage_drop(oriented_tree, nodes, dist_matrix, tiers) + # if warnings: + # logger.warning(f"[VDROP] {len(warnings)} edges still exceed threshold after fix") + + + cables = oriented_tree_to_grid(oriented_tree, dist_matrix,nodes, EXTRA_CABLE_LENGTH_M, vdrop=vdrop) + logger.debug("Assigned tiers from MST: %s", tiers) + + computed_cables = _compute_power_flow(cables, grid.nodes) + node_phase, cable_phase, phase_loads = assign_phases_from_grid(grid.nodes, computed_cables, logger) + + for n in node_names: + nodes[n].phase = node_phase.get(n, 0) + + for cable in computed_cables: + cable.phase = cable_phase.get((cable.from_node, cable.to_node), 0) + + grid = PowerGrid( + nodes=nodes, + cables={cable.id: cable for cable in computed_cables}, + ) + grid.phase_loads = phase_loads + + _compute_distro_requirements(grid) + return grid + +def fix_voltage_drop_warnings( + oriented_tree: nx.DiGraph, + nodes: dict, + dist_matrix: dict, + warnings: list, + cable_tiers: dict, +) -> nx.DiGraph: + """ + For each warning edge, try to find a closer parent for the child node. + Returns a modified tree. + """ + if not warnings: + return oriented_tree + + tree = oriented_tree.copy() + generator = next(n for n, node in nodes.items() if node.is_generator) + node_names = list(nodes.keys()) + + for w in warnings: + node_i, node_j = w["edge"] + logger.info(f"[VDROP FIX] Trying to re-route {node_i}→{node_j} ({w['vdrop_pct']}%)") + + # Find alternative parents: nodes already in tree, closer to node_j + current_dist = dist_matrix[(node_i, node_j)] + candidates = [ + n for n in node_names + if n != node_j + and n in tree.nodes + and dist_matrix[(n, node_j)] < current_dist + and not nx.has_path(tree, node_j, n) # avoid cycles + ] + candidates.sort(key=lambda n: dist_matrix[(n, node_j)]) + + for candidate in candidates: + # Check if rerouting fixes the voltage drop + new_flow = sum( + nodes[n].power_watts + for n in nx.descendants(tree, node_j) | {node_j} + ) + new_d = dist_matrix[(candidate, node_j)] + tier = cable_tiers.get((node_i, node_j)) + if tier: + new_vdrop = 100 * (new_flow / (V0 * PF)) * (RHO_COPPER / tier.area_mm2) * new_d / V0 + if new_vdrop <= VDROP_THRESHOLD_PERCENT: + # Apply reroute + tree.remove_edge(node_i, node_j) + tree.add_edge(candidate, node_j) + logger.info( + f"[VDROP FIX] Re-routed {node_j}: " + f"{node_i}→{node_j} ({w['vdrop_pct']:.1f}%) " + f"replaced by {candidate}→{node_j} ({new_vdrop:.1f}%)" + ) + break + else: + logger.warning( + f"[VDROP FIX] No fix found for {node_i}→{node_j}, " + f"consider upgrading cable tier or splitting the branch" + ) + + return tree + +def compute_voltage_drop( + oriented_tree: nx.DiGraph, + nodes: dict, + dist_matrix: dict, + cable_tiers: dict, # (from, to) → CableTier, resultado de assign_tiers_to_nodes +) -> dict: + """Returns {(from, to): vdrop_percent} for each edge.""" + generator = next(n for n, node in nodes.items() if node.is_generator) + + # Compute cumulative flow (watts) for each edge = sum of all descendant loads + cumulative_flow = {} + for node_i, node_j in nx.bfs_edges(oriented_tree, source=generator): + subtree_load = sum( + nodes[n].power_watts + for n in nx.descendants(oriented_tree, node_j) | {node_j} + if not nodes[n].is_generator + ) + cumulative_flow[(node_i, node_j)] = subtree_load + + # Compute voltage drop per edge + vdrop = {} + warnings = [] + for (node_i, node_j), flow_w in cumulative_flow.items(): + tier = cable_tiers.get((node_i, node_j)) + if tier is None: + continue + d = dist_matrix[(node_i, node_j)] + resistance_total = (RHO_COPPER / tier.area_mm2) * d # ohms + current_a = flow_w / (V0 * PF) + vdrop_v = current_a * resistance_total + vdrop_pct = 100 * vdrop_v / V0 + + vdrop[(node_i, node_j)] = vdrop_pct + + if vdrop_pct > VDROP_THRESHOLD_PERCENT: + warnings.append({ + "edge": (node_i, node_j), + "vdrop_pct": round(vdrop_pct, 2), + "flow_w": round(flow_w), + "tier": tier.area_mm2, + "distance_m": round(d), + }) + logger.warning( + f"[VDROP] {node_i}→{node_j}: {vdrop_pct:.1f}% " + f"(flow={flow_w:.0f}W, d={d:.0f}m, tier={tier.area_mm2}mm²)" + ) + + return vdrop, warnings + +def is_distro(name: str) -> bool: + return name.lower().startswith('distro') + +def precompute_hub(nodes, node_names, dist_matrix): + hub_score = {} + for n in node_names: + if nodes[n].is_generator: + continue + nearby_load = sum( + nodes[m].power_watts + for m in node_names + if m != n and not nodes[m].is_generator + and dist_matrix[(n, m)] <= RADIUS_M + ) + hub_score[n] = nearby_load + return hub_score + +def oriented_tree_to_grid( + oriented_tree: nx.DiGraph, + dist_matrix: dict, + nodes: dict, + extra_cable_m: float = EXTRA_CABLE_LENGTH_M, + vdrop =None, +) -> PowerGrid: + + cables = [] + for cid, (node_i, node_j) in enumerate(oriented_tree.edges()): + d = dist_matrix[(node_i, node_j)] + cable = Cable( + id=f"opt_{cid}", + from_node=node_i, + to_node=node_j, + length_m=d + extra_cable_m, + from_coords=(nodes[node_i].lon, nodes[node_i].lat), + to_coords=(nodes[node_j].lon, nodes[node_j].lat), + ) + if vdrop: + cable.vdrop_pct = round(vdrop.get((node_i, node_j), 0), 2) + cable.vdrop_warning = cable.vdrop_pct > VDROP_THRESHOLD_PERCENT + cables.append(cable) + return cables + +# def assign_tiers_to_nodes(tree, nodes, dist_matrix=None): +# tiers = nx.single_source_shortest_path_length(tree, list(tree.nodes)[0]) +# for node_name, tier in tiers.items(): +# node = nodes.get(node_name) +# if node is None: +# continue +# try: +# node.tier = tier +# except AttributeError: +# pass +# return tiers + +def assign_tiers_to_nodes( + tree: nx.DiGraph, + nodes: dict, + dist_matrix: dict, +) -> dict: + """ + Assigns the minimum cable tier to each edge that satisfies: + - current capacity >= cumulative flow + - voltage drop <= threshold + Returns {(from, to): CableTier} + """ + generator = next(n for n, node in nodes.items() if node.is_generator) + cable_tiers = {} + + for node_i, node_j in nx.bfs_edges(tree, source=generator): + # Cumulative flow = sum of all descendant loads + subtree_nodes = nx.descendants(tree, node_j) | {node_j} + flow_w = sum( + nodes[n].power_watts + for n in subtree_nodes + if not nodes[n].is_generator + ) + current_a = flow_w / (V0 * PF) + d = dist_matrix[(node_i, node_j)] + + # Select minimum tier that satisfies both constraints + selected_tier = None + for tier in sorted(_CABLE_TYPES, key=lambda t: t.area_mm2): # smallest first + # Capacity check + if tier.max_current_a < current_a: + continue + # Voltage drop check + resistance_total = (RHO_COPPER / tier.area_mm2) * d + vdrop_pct = 100 * current_a * resistance_total / V0 + if vdrop_pct > VDROP_THRESHOLD_PERCENT: + continue + selected_tier = tier + break + + if selected_tier is None: + # No tier satisfies constraints — use largest available and warn + selected_tier = max(_CABLE_TYPES, key=lambda t: t.area_mm2) + logger.warning( + f"[TIER] No suitable tier for {node_i}→{node_j}: " + f"flow={flow_w:.0f}W, current={current_a:.1f}A, d={d:.0f}m — " + f"using largest tier ({selected_tier.area_mm2}mm²)" + ) + + cable_tiers[(node_i, node_j)] = selected_tier + logger.debug( + f"[TIER] {node_i}→{node_j}: {selected_tier.area_mm2}mm² " + f"({current_a:.1f}A, {d:.0f}m)" + ) + + return cable_tiers + +def define_distance_matrix(nodes, names, dist_matrix): + for a, b in itertools.permutations(names, 2): + na = nodes[a] + nb = nodes[b] + d = geodesic_distance_m( + na.lon, + na.lat, + nb.lon, + nb.lat, + ) + dist_matrix[(a, b)] = d diff --git a/src/nopywer/ortools_solverV1.py b/src/nopywer/ortools_solverV1.py new file mode 100644 index 0000000..8bac198 --- /dev/null +++ b/src/nopywer/ortools_solverV1.py @@ -0,0 +1,377 @@ +import itertools +import logging +import os + +from ortools.sat.python import cp_model + +from nopywer import inventory +from nopywer.analyze import _compute_distro_requirements +from nopywer.constants import EXTRA_CABLE_LENGTH_M, PF, RHO_COPPER, V0, VDROP_THRESHOLD_PERCENT +from nopywer.geometry import geodesic_distance_m +from nopywer.models import _CABLE_TYPES, Cable, PowerGrid +from nopywer.optimization_tools import assign_phases_from_grid, segments_cross +from nopywer.optimize import _compute_power_flow + +logger = logging.getLogger(__name__) +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + datefmt="%m-%d %H:%M", + filename="optimizer.log", + filemode="w", +) +console = logging.StreamHandler() +console.setLevel(logging.INFO) +formatter = logging.Formatter("%(name)-12s: %(levelname)-8s %(message)s") +console.setFormatter(formatter) +logging.getLogger().addHandler(console) + + +MAX_POWER = 100000 + +# NOPYWER_INPUT='tests/fixtures/input_nodes.geojson' +# NOPYWER_OUTPUT='output.geojson' +# NOPYWER_INVENTORY='orange_inventory.xlsx' + +INVENTORY = os.getenv("NOPYWER_INVENTORY", "orange_inventory.xlsx") + +# TODO: add max number of connections for each distro/node + +# basically, for any grid generated by the optim algo: +# you need to compute the voltage drop, current in each cables etc. +# So phases need to be assigned first. Then you check if the grid is valid +# then, only for those feasible solutions, you try to optimize voltage drop, total length, etc + + +def optimize_layout( + grid: PowerGrid, + extra_cable_m: float = EXTRA_CABLE_LENGTH_M, +) -> PowerGrid: + + ITERATE_ACCORDING_TO_INVENTORY = False + nodes = grid.nodes + node_names = list(nodes.keys()) + generator = grid.generator.name + + dist_matrix = {} + define_distance_matrix(nodes, node_names, dist_matrix) + + model = cp_model.CpModel() + + connection_variables = define_connection_variables(node_names, model) + flow = define_flow_variables(node_names, model) + + depth = {n: model.NewIntVar(0, len(node_names), f"depth_{n}") for n in node_names} + model.Add(depth[generator] == 0) + + tier_variables = {} + create_depth_constraint(node_names, model, connection_variables, depth) + add_cable_tier_variables(node_names, model, tier_variables, nodes) + tier_selection(node_names, model, connection_variables, tier_variables, nodes) + add_cable_capacity_constraints(node_names, model, flow, tier_variables, nodes) + add_flow_cable_exists_constraints(node_names, model, flow, tier_variables, nodes) + add_no_crossing_constraints(node_names, nodes, model, connection_variables) + add_nodes_proximity_constraints(node_names, nodes, model, connection_variables, dist_matrix) + + for n in nodes: + if n == generator: + continue + model.Add(sum(connection_variables[(i, n)] for i in node_names if i != n) == 1) + + add_power_balance_constraints(nodes, node_names, generator, model, flow) + + generator_connections = [ + connection_variables[(generator, j)] for j in node_names if j != generator + ] + direct_connections_count = model.NewIntVar(0, len(node_names), "direct_connections") + model.Add(direct_connections_count == sum(generator_connections)) + + VOLTAGE_SCALE = 100 + RADIALITY_WEIGHT = 100000 + + cost_terms = [] + cost_terms.append(direct_connections_count * RADIALITY_WEIGHT) + + for node_i in node_names: + for node_j in node_names: + if node_i == node_j: + continue + if nodes[node_j].is_generator: # never towards generator + continue + d = dist_matrix[(node_i, node_j)] + + # Select preferably smaller cable tiers + for cable_type, tier in enumerate(_CABLE_TYPES): + cost_terms.append( + int(d * tier.tier_cost * 100) * tier_variables[(node_i, node_j, cable_type)] + ) + + # Minimize voltage drop: flow * resistance <= Vdrop_threshold + resistance_per_m = RHO_COPPER / tier.area_mm2 + rhs = int(VDROP_THRESHOLD_PERCENT / 100 * V0 * PF * VOLTAGE_SCALE) + model.Add( + flow[(node_i, node_j)] * int(resistance_per_m * d * VOLTAGE_SCALE) <= rhs + ).OnlyEnforceIf(tier_variables[(node_i, node_j, cable_type)]) + + # Penalize large direct connections from the gennie + if nodes[node_i].is_generator: + cost_terms.append(int(d * 20) * connection_variables[(node_i, node_j)]) + else: + # Help minimize total length + cost_terms.append(int(d * d / 1000) * connection_variables[(node_i, node_j)]) + + model.Minimize(sum(cost_terms)) + solver = cp_model.CpSolver() + solver.parameters.max_time_in_seconds = 120 + solver.parameters.log_search_progress = False + solver.parameters.num_search_workers = 8 + solver.parameters.cp_model_presolve = True + status = solver.Solve(model) + + if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE): + print("No solution found. Retrying with relaxed constraints...") + raise RuntimeError("Optimization failed") + + _compute_distro_requirements(grid) + grid = assign_nodes_and_cables( + grid, extra_cable_m, nodes, node_names, dist_matrix, connection_variables, solver + ) + + while status in (cp_model.OPTIMAL, cp_model.FEASIBLE): + if ITERATE_ACCORDING_TO_INVENTORY: + # analyze_grid(grid, verbose=True) + unmatched_cables = inventory.choose_cables(INVENTORY, grid.cables) + # inventory.choose_distros(INVENTORY, grid.nodes) + if unmatched_cables: + for unm in unmatched_cables: + nodes = [unm.from_node, unm.to_node] + print(f"\t {unm.plugs_and_sockets_a}{'A':.<4}") + print(f"\t {unm.length_m:.0f}m) {'-'.join(nodes)}") + print("Unmatched cables found in inventory, re-optimizing...") + ITERATE_ACCORDING_TO_INVENTORY = True + + model.AddBoolOr( + [ + connection_variables[(i, j)].Not() + if solver.Value(connection_variables[(i, j)]) == 1 + else connection_variables[(i, j)] + for i in node_names + for j in node_names + if i != j + ] + ) + status = solver.Solve(model) + else: + print("All cables matched with inventory. Optimization complete.") + ITERATE_ACCORDING_TO_INVENTORY = False + + else: + break + + total_cable_length_m = ( + round( + sum(c.length_m for c in grid.cables.values()), + 1, + ), + ) + num_cables = len(grid.cables) + logger.info(f"TOTAL DISTANCE: {total_cable_length_m}") + logger.info(f"TOTAL CABLES: {num_cables}") + + return grid + + +def add_flow_cable_exists_constraints(node_names, model, flow, tier_variables, nodes): + for node_i in node_names: + for node_j in node_names: + if node_i == node_j: + continue + if nodes[node_j].is_generator: # nunca hacia el generador + continue + model.Add( + flow[(node_i, node_j)] + <= sum( + tier.max_current_a * V0 * tier_variables[(node_i, node_j, t)] + for t, tier in enumerate(_CABLE_TYPES) + ) + ) + + +def add_nodes_proximity_constraints(node_names, nodes, model, connection_variables, dist_matrix): + for node_i in node_names: + for node_j in node_names: + if node_i >= node_j: + continue + d_ij = dist_matrix[(node_i, node_j)] + + for node_k in node_names: + if node_k == node_i or node_k == node_j: + continue + # Si k está más lejos de ambos que i y j entre sí + if dist_matrix[(node_k, node_i)] > d_ij and dist_matrix[(node_k, node_j)] > d_ij: + # No pueden ambos conectarse a k + model.Add( + connection_variables[(node_k, node_i)] + + connection_variables[(node_k, node_j)] + <= 1 + ) + + +def assign_nodes_and_cables( + grid, extra_cable_m, nodes, node_names, dist_matrix, connection_variables, solver +): + cid = 0 + cables: list[Cable] = [] + total_distance = [] + + for node_i in node_names: + for node_j in node_names: + if node_i == node_j: + continue + + if solver.Value(connection_variables[(node_i, node_j)]): + length = dist_matrix[(node_i, node_j)] + extra_cable_m + total_distance.append(length) + + cable = Cable( + id=f"opt_{cid}", + length_m=length + extra_cable_m, + from_node=node_i, + to_node=node_j, + from_coords=(nodes[node_i].lon, nodes[node_i].lat), + to_coords=(nodes[node_j].lon, nodes[node_j].lat), + ) + cables.append(cable) + cid += 1 + + computed_cables = _compute_power_flow(cables, nodes) + node_phase, cable_phase, phase_loads = assign_phases_from_grid(nodes, computed_cables) + for n in node_names: + nodes[n].phase = node_phase.get(n, 0) + grid.nodes = nodes + + for cable in computed_cables: + cable.phase = cable_phase.get((cable.from_node, cable.to_node), 0) + + logger.info(f"[PHASE] phase loads: {phase_loads}") + grid.phase_loads = phase_loads + grid.cables = {cable.id: cable for cable in computed_cables} + return grid + + +def add_no_crossing_constraints(node_names, nodes, model, connection_variables): + crossing_pairs = 0 + for i, j, k, z in itertools.permutations(node_names, 4): + if i >= k: # prevents duplicates + continue + p1 = (nodes[i].lon, nodes[i].lat) + p2 = (nodes[j].lon, nodes[j].lat) + p3 = (nodes[k].lon, nodes[k].lat) + p4 = (nodes[z].lon, nodes[z].lat) + + if segments_cross(p1, p2, p3, p4): + # if these two edges cross, they can't both be in the solution + model.Add(connection_variables[(i, j)] + connection_variables[(k, z)] <= 1) + crossing_pairs += 1 + print(f"[CROSSING] pares de cruces prohibidos: {crossing_pairs}") + + +def create_depth_constraint(node_names, model, connection_v, depth): + # depth -distance from generator - constraint to prevent cycles + N = len(node_names) + for node_i in node_names: + for node_j in node_names: + if node_i == node_j: + continue + model.Add(depth[node_j] >= depth[node_i] + 1).OnlyEnforceIf( + connection_v[(node_i, node_j)] + ) + model.Add(depth[node_j] <= depth[node_i] + 1 + N * (1 - connection_v[(node_i, node_j)])) + + +def add_cable_capacity_constraints(node_names, model, flow, tier_variable, nodes): + for node_i in node_names: + for node_j in node_names: + if node_i == node_j: + continue + if nodes[node_j].is_generator: + continue + + for cable_type, tier in enumerate(_CABLE_TYPES): + model.Add( + flow[(node_i, node_j)] <= int(tier.max_current_a * V0 * PF) + ).OnlyEnforceIf(tier_variable[(node_i, node_j, cable_type)]) + + +def tier_selection(node_names, model, connection_v, tier_variable, nodes): + for node_i in node_names: + for node_j in node_names: + if node_i == node_j: + continue + if nodes[node_j].is_generator: + continue + model.Add( + sum(tier_variable[(node_i, node_j, t)] for t in range(len(_CABLE_TYPES))) + == connection_v[(node_i, node_j)] + ) + + +def define_flow_variables(node_names, model): + flow = {} + for node_i in node_names: + for node_j in node_names: + if node_i != node_j: + flow[(node_i, node_j)] = model.NewIntVar( + 0, + MAX_POWER, + f"flow_{node_i}_{node_j}", + ) + + return flow + + +def define_connection_variables(node_names, model): + connection_v = {} + for node_i in node_names: + for node_j in node_names: + if node_i != node_j: + connection_v[(node_i, node_j)] = model.NewBoolVar(f"x_{node_i}_{node_j}") + return connection_v + + +def add_power_balance_constraints(nodes, node_names, generator, model, flow): + total_load = int(sum(n.power_watts for n in nodes.values() if not n.is_generator)) + for n in node_names: + incoming = [flow[(i, n)] for i in node_names if i != n] + outgoing = [flow[(n, j)] for j in node_names if j != n] + if n == generator: + model.Add(sum(outgoing) - sum(incoming) == total_load) + else: + model.Add(sum(incoming) - sum(outgoing) == int(nodes[n].power_watts)) + print("Total load:", total_load) + + +def add_cable_tier_variables(node_names, model, tier_variable, nodes): + for node_i in node_names: + for node_j in node_names: + if node_i == node_j: + continue + if nodes[node_j].is_generator: # never towards generator + continue + for cable_type in range(len(_CABLE_TYPES)): + tier_variable[(node_i, node_j, cable_type)] = model.NewBoolVar( + f"tier_{cable_type}_{node_i}_{node_j}" + ) + + +def define_distance_matrix(nodes, names, dist_matrix): + for a, b in itertools.permutations(names, 2): + na = nodes[a] + nb = nodes[b] + d = geodesic_distance_m( + na.lon, + na.lat, + nb.lon, + nb.lat, + ) + dist_matrix[(a, b)] = d diff --git a/telemetry data/telemetry visualizer.py b/telemetry data/telemetry visualizer.py new file mode 100644 index 0000000..9f3b26f --- /dev/null +++ b/telemetry data/telemetry visualizer.py @@ -0,0 +1,57 @@ +from pathlib import Path + +import pandas as pd +import plotly.express as px + + +def load_telemetry(file_path): + phases = {} + current_phase = None + rows = [] + + with open(file_path, encoding="utf-8-sig") as f: + for line in f: + line = line.strip() + + if line.startswith("Phase"): + if current_phase and rows: + df_phase = pd.DataFrame(rows, columns=["Time", "Wh"]) + phases[current_phase] = df_phase + rows = [] + current_phase = line + + elif line and not line.startswith("Time"): + parts = line.split(" , ") + if len(parts) == 2: + rows.append(parts) + + if current_phase and rows: + df_phase = pd.DataFrame(rows, columns=["Time", "Wh"]) + phases[current_phase] = df_phase + return phases + + +def clean_phase_data(df): + df["Time"] = pd.to_datetime(df["Time"], dayfirst=True) + df["Wh"] = pd.to_numeric(df["Wh"]) + return df + + +DATA_DIR = Path("./telemetry data") +files = list(DATA_DIR.glob("*.csv")) +all_data = [] + +for file in files: + phases = load_telemetry(file) + + for phase_name, df in phases.items(): + df = clean_phase_data(df) + df["phase"] = phase_name + df["source"] = file.name.split("_")[0] + all_data.append(df) + +df = pd.concat(all_data) + +fig = px.line(df, x="Time", y="Wh", color="phase", facet_row="source", title="Power telemetry") + +fig.show() diff --git a/tests/fixtures/export.geojson b/tests/fixtures/export.geojson new file mode 100644 index 0000000..f24d531 --- /dev/null +++ b/tests/fixtures/export.geojson @@ -0,0 +1,1417 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13953071992767785, + 41.6999752927288 + ], + [ + -0.13989663973096164, + 41.69911453243565 + ] + ] + }, + "properties": { + "id": "cable_0", + "nodes": [ + "distro9", + "distro8" + ], + "from": "distro9", + "to": "distro8", + "length_m": 110.4, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 30.6, + "cum_power_kw": 6.33, + "vdrop_volts": 21.66 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.14053148336696134, + 41.701600130185604 + ], + [ + -0.13988015097303447, + 41.701130634599245 + ] + ] + }, + "properties": { + "id": "cable_1", + "nodes": [ + "zaz", + "generator" + ], + "from": "zaz", + "to": "generator", + "length_m": 85.2, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 81.2, + "cum_power_kw": 16.8, + "vdrop_volts": 44.33 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.139532242707377, + 41.69998389335092 + ], + [ + -0.14007635726339934, + 41.69963441734 + ] + ] + }, + "properties": { + "id": "cable_2", + "nodes": [ + "distro9", + "curious creatures" + ], + "from": "distro9", + "to": "curious creatures", + "length_m": 69.7, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 24.2, + "cum_power_kw": 5.0, + "vdrop_volts": 25.9 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.14072105157337192, + 41.702002684355925 + ], + [ + -0.14052755675000794, + 41.70162945801931 + ] + ] + }, + "properties": { + "id": "cable_3", + "nodes": [ + "garden of joy", + "zaz" + ], + "from": "garden of joy", + "to": "zaz", + "length_m": 54.5, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 48.3, + "cum_power_kw": 10.0, + "vdrop_volts": 40.51 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13989743376584954, + 41.69911326307554 + ], + [ + -0.1394391591861685, + 41.6989932968759 + ] + ] + }, + "properties": { + "id": "cable_4", + "nodes": [ + "distro8", + "planet pulpo" + ], + "from": "distro8", + "to": "planet pulpo", + "length_m": 50.4, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 9.7, + "cum_power_kw": 2.0, + "vdrop_volts": 7.49 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13989662913899972, + 41.69911360709415 + ], + [ + -0.14028018307834664, + 41.698835653220975 + ] + ] + }, + "properties": { + "id": "cable_5", + "nodes": [ + "distro8", + "wonderever" + ], + "from": "distro8", + "to": "wonderever", + "length_m": 54.4, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 6.8, + "cum_power_kw": 1.4, + "vdrop_volts": 5.66 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13983961469861161, + 41.69878117723728 + ], + [ + -0.1398980040638073, + 41.69911389878753 + ] + ] + }, + "properties": { + "id": "cable_6", + "nodes": [ + "oasis playground", + "distro8" + ], + "from": "oasis playground", + "to": "distro8", + "length_m": 47.3, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 14.5, + "cum_power_kw": 3.0, + "vdrop_volts": 10.54 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13989674641519334, + 41.69911370103021 + ], + [ + -0.1403477956168545, + 41.69906542845324 + ] + ] + }, + "properties": { + "id": "cable_7", + "nodes": [ + "distro8", + "mirage" + ], + "from": "distro8", + "to": "mirage", + "length_m": 47.9, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 6.8, + "cum_power_kw": 1.4, + "vdrop_volts": 4.98 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13989090923520467, + 41.70058300170854 + ], + [ + -0.1403111131585792, + 41.70021466955112 + ] + ] + }, + "properties": { + "id": "cable_8", + "nodes": [ + "distro1", + "eyao" + ], + "from": "distro1", + "to": "eyao", + "length_m": 63.8, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 9.7, + "cum_power_kw": 2.0, + "vdrop_volts": 9.48 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13990617156460367, + 41.69912572222792 + ], + [ + -0.14031724994819922, + 41.69938775779702 + ] + ] + }, + "properties": { + "id": "cable_9", + "nodes": [ + "distro8", + "pdr of fiascostan" + ], + "from": "distro8", + "to": "pdr of fiascostan", + "length_m": 54.9, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 9.7, + "cum_power_kw": 2.0, + "vdrop_volts": 8.16 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13989129991538604, + 41.7005836394078 + ], + [ + -0.14032125920402552, + 41.70076539629761 + ] + ] + }, + "properties": { + "id": "cable_10", + "nodes": [ + "distro1", + "glowy owl" + ], + "from": "distro1", + "to": "glowy owl", + "length_m": 51.1, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 9.7, + "cum_power_kw": 2.0, + "vdrop_volts": 7.59 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1398907790084798, + 41.70058278914213 + ], + [ + -0.1404629446182906, + 41.70047922303174 + ] + ] + }, + "properties": { + "id": "cable_11", + "nodes": [ + "distro1", + "mini camp" + ], + "from": "distro1", + "to": "mini camp", + "length_m": 59.0, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 4.8, + "cum_power_kw": 1.0, + "vdrop_volts": 4.38 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1398839389103155, + 41.70113013731505 + ], + [ + -0.14061737924151105, + 41.70110305493381 + ] + ] + }, + "properties": { + "id": "cable_12", + "nodes": [ + "generator", + "barrio espace" + ], + "from": "generator", + "to": "barrio espace", + "length_m": 71.1, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 14.5, + "cum_power_kw": 3.0, + "vdrop_volts": 15.85 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.139874356320217, + 41.70012865495601 + ], + [ + -0.13951696030729716, + 41.70022112725018 + ] + ] + }, + "properties": { + "id": "cable_13", + "nodes": [ + "glitch", + "distro2" + ], + "from": "glitch", + "to": "distro2", + "length_m": 41.5, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 19.3, + "cum_power_kw": 4.0, + "vdrop_volts": 12.34 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1405228605103245, + 41.70162137103771 + ], + [ + -0.14105370474606616, + 41.70178357945931 + ] + ] + }, + "properties": { + "id": "cable_14", + "nodes": [ + "zaz", + "ran'dome" + ], + "from": "zaz", + "to": "ran'dome", + "length_m": 57.7, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 19.3, + "cum_power_kw": 4.0, + "vdrop_volts": 17.15 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1405247969713634, + 41.7016202411872 + ], + [ + -0.14099377105678132, + 41.701384425977956 + ] + ] + }, + "properties": { + "id": "cable_15", + "nodes": [ + "zaz", + "desert dessert" + ], + "from": "zaz", + "to": "desert dessert", + "length_m": 57.0, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 32.8, + "cum_power_kw": 6.8, + "vdrop_volts": 28.81 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13964289449621192, + 41.70127410398372 + ], + [ + -0.13945297743107815, + 41.70160010569251 + ] + ] + }, + "properties": { + "id": "cable_16", + "nodes": [ + "rdtfgh", + "barrio del sol" + ], + "from": "rdtfgh", + "to": "barrio del sol", + "length_m": 49.5, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 17.8, + "cum_power_kw": 3.68, + "vdrop_volts": 13.54 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.14001492449097064, + 41.69989479508128 + ], + [ + -0.13951296635310073, + 41.70022530910071 + ] + ] + }, + "properties": { + "id": "cable_17", + "nodes": [ + "belly town", + "distro2" + ], + "from": "belly town", + "to": "distro2", + "length_m": 65.6, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 14.5, + "cum_power_kw": 3.0, + "vdrop_volts": 14.62 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13989755914393132, + 41.69911388770567 + ], + [ + -0.13914562055063537, + 41.69927447838743 + ] + ] + }, + "properties": { + "id": "cable_18", + "nodes": [ + "distro8", + "olive odissey" + ], + "from": "distro8", + "to": "olive odissey", + "length_m": 75.1, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 9.7, + "cum_power_kw": 2.0, + "vdrop_volts": 11.16 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13990704758599418, + 41.69912641093495 + ], + [ + -0.14039843147426054, + 41.69928042564868 + ] + ] + }, + "properties": { + "id": "cable_19", + "nodes": [ + "distro8", + "jamhouse" + ], + "from": "distro8", + "to": "jamhouse", + "length_m": 54.3, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 14.5, + "cum_power_kw": 3.0, + "vdrop_volts": 12.1 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13990606235151692, + 41.69912544646274 + ], + [ + -0.13989660696530037, + 41.69911410712428 + ] + ] + }, + "properties": { + "id": "cable_20", + "nodes": [ + "distro8", + "distro8" + ], + "from": "distro8", + "to": "distro8", + "length_m": 11.5, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 0.0, + "cum_power_kw": 0.0, + "vdrop_volts": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13951464719975476, + 41.70023034587373 + ], + [ + -0.13989025810158784, + 41.70058193887645 + ] + ] + }, + "properties": { + "id": "cable_21", + "nodes": [ + "distro2", + "distro1" + ], + "from": "distro2", + "to": "distro1", + "length_m": 60.0, + "area_mm2": 16.0, + "plugs_and_sockets_a": 63.0, + "cable_type": "1P 63A \u2014 16.0mm\u00b2", + "current_a": 63.1, + "cum_power_kw": 13.07, + "vdrop_volts": 9.1 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1395212435711459, + 41.69999574982771 + ], + [ + -0.1395055790930972, + 41.700220130174756 + ] + ] + }, + "properties": { + "id": "cable_22", + "nodes": [ + "distro9", + "distro2" + ], + "from": "distro9", + "to": "distro2", + "length_m": 35.0, + "area_mm2": 16.0, + "plugs_and_sockets_a": 63.0, + "cable_type": "1P 63A \u2014 16.0mm\u00b2", + "current_a": 56.4, + "cum_power_kw": 11.67, + "vdrop_volts": 4.74 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1398907032137395, + 41.700583973416585 + ], + [ + -0.13987627447357368, + 41.70111958767342 + ] + ] + }, + "properties": { + "id": "cable_23", + "nodes": [ + "distro1", + "generator" + ], + "from": "distro1", + "to": "generator", + "length_m": 69.5, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 77.6, + "cum_power_kw": 16.07, + "vdrop_volts": 82.99 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13987287159693085, + 41.70112776842358 + ], + [ + -0.1396476485857442, + 41.70127259284582 + ] + ] + }, + "properties": { + "id": "cable_24", + "nodes": [ + "generator", + "rdtfgh" + ], + "from": "generator", + "to": "rdtfgh", + "length_m": 34.7, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 17.8, + "cum_power_kw": 3.68, + "vdrop_volts": 9.49 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14046027066882047, + 41.70048109809566 + ] + }, + "properties": { + "name": "mini camp", + "type": "load", + "power": 1000.0, + "cum_power": 1000.0, + "voltage": 142.6, + "vdrop_percent": 38.0, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1403126048615206, + 41.70021165965277 + ] + }, + "properties": { + "name": "eyao", + "type": "load", + "power": 2000.0, + "cum_power": 2000.0, + "voltage": 137.5, + "vdrop_percent": 40.22, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13986458645846486, + 41.70012838821719 + ] + }, + "properties": { + "name": "glitch", + "type": "load", + "power": 4000.0, + "cum_power": 4000.0, + "voltage": 125.6, + "vdrop_percent": 45.39, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14001451226101178, + 41.69988172981668 + ] + }, + "properties": { + "name": "belly town", + "type": "load", + "power": 3000.0, + "cum_power": 3000.0, + "voltage": 123.3, + "vdrop_percent": 46.39, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14032067533151324, + 41.699387585580126 + ] + }, + "properties": { + "name": "pdr of fiascostan", + "type": "load", + "power": 2000.0, + "cum_power": 2000.0, + "voltage": 103.3, + "vdrop_percent": 55.09, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14037095139108816, + 41.69906229826065 + ] + }, + "properties": { + "name": "mirage", + "type": "load", + "power": 1400.0, + "cum_power": 1400.0, + "voltage": 106.5, + "vdrop_percent": 53.7, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1402769285712588, + 41.69884015144679 + ] + }, + "properties": { + "name": "wonderever", + "type": "load", + "power": 1400.0, + "cum_power": 1400.0, + "voltage": 105.8, + "vdrop_percent": 54.0, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13983096674250983, + 41.698769513641636 + ] + }, + "properties": { + "name": "oasis playground", + "type": "load", + "power": 3000.0, + "cum_power": 3000.0, + "voltage": 101.0, + "vdrop_percent": 56.09, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1394393620604287, + 41.69898872265349 + ] + }, + "properties": { + "name": "planet pulpo", + "type": "load", + "power": 2000.0, + "cum_power": 2000.0, + "voltage": 104.0, + "vdrop_percent": 54.78, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1400824723373084, + 41.699631722152795 + ] + }, + "properties": { + "name": "curious creatures", + "type": "load", + "power": 10000.0, + "cum_power": 10000.0, + "voltage": 107.3, + "vdrop_percent": 53.35, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1403046266028607, + 41.700765329482905 + ] + }, + "properties": { + "name": "glowy owl", + "type": "load", + "power": 2000.0, + "cum_power": 2000.0, + "voltage": 139.4, + "vdrop_percent": 39.39, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14061462479929102, + 41.701104168536816 + ] + }, + "properties": { + "name": "barrio espace", + "type": "load", + "power": 3000.0, + "cum_power": 3000.0, + "voltage": 214.2, + "vdrop_percent": 6.87, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14105640391758656, + 41.70179263095132 + ] + }, + "properties": { + "name": "ran'dome", + "type": "load", + "power": 4000.0, + "cum_power": 4000.0, + "voltage": 168.5, + "vdrop_percent": 26.74, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14100409056756252, + 41.70136886602274 + ] + }, + "properties": { + "name": "desert dessert", + "type": "load", + "power": 6800.0, + "cum_power": 6800.0, + "voltage": 156.9, + "vdrop_percent": 31.78, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1407273698750896, + 41.701997572463505 + ] + }, + "properties": { + "name": "garden of joy", + "type": "load", + "power": 30000.0, + "cum_power": 30000.0, + "voltage": 145.2, + "vdrop_percent": 36.87, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1394518880437417, + 41.701599503491636 + ] + }, + "properties": { + "name": "barrio del sol", + "type": "load", + "power": 3680.0, + "cum_power": 3680.0, + "voltage": 207.0, + "vdrop_percent": 10.0, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1391394682682699, + 41.69927405035179 + ] + }, + "properties": { + "name": "olive odissey", + "type": "load", + "power": 2000.0, + "cum_power": 2000.0, + "voltage": 100.3, + "vdrop_percent": 56.39, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13952647308927593, + 41.69998517673939 + ] + }, + "properties": { + "name": "distro9", + "type": "load", + "power": 1000.0, + "cum_power": 26800.0, + "voltage": 133.2, + "vdrop_percent": 42.09, + "distro": { + "in": "3P 63.0A", + "out": { + "3P 32.0A": 1, + "1P 16.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13987842843560366, + 41.701128430139995 + ] + }, + "properties": { + "name": "generator", + "type": "generator", + "power": 0.0, + "cum_power": 88280.0, + "voltage": 230, + "vdrop_percent": 0.0, + "distro": { + "in": "3P 125A", + "out": { + "3P 32.0A": 1, + "1P 16.0A": 3 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1398906258410024, + 41.70058336835788 + ] + }, + "properties": { + "name": "distro1", + "type": "load", + "power": 0.0, + "cum_power": 38800.0, + "voltage": 147.0, + "vdrop_percent": 36.09, + "distro": { + "in": "1P 16.0A", + "out": { + "1P 16.0A": 3, + "3P 63.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14052384215401315, + 41.70162166285516 + ] + }, + "properties": { + "name": "distro-zaz", + "type": "load", + "power": 1000.0, + "cum_power": 41800.0, + "voltage": 185.7, + "vdrop_percent": 19.26, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 3 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13964561238814718, + 41.70127284174622 + ] + }, + "properties": { + "name": "distro-rdtfgh", + "type": "load", + "power": 1000.0, + "cum_power": 4680.0, + "voltage": 220.5, + "vdrop_percent": 4.13, + "distro": { + "in": "1P 16.0A", + "out": { + "1P 16.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13989622438430727, + 41.69911385446002 + ] + }, + "properties": { + "name": "distro8", + "type": "load", + "power": 1000.0, + "cum_power": 15800.0, + "voltage": 111.5, + "vdrop_percent": 51.52, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 7 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13951008153297134, + 41.70022595079415 + ] + }, + "properties": { + "name": "distro2", + "type": "load", + "power": 0.0, + "cum_power": 33800.0, + "voltage": 137.9, + "vdrop_percent": 40.04, + "distro": { + "in": "3P 63.0A", + "out": { + "1P 16.0A": 2, + "3P 63.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1398945488962474, + 41.700573358856026 + ] + }, + "properties": { + "name": "distro", + "type": "load", + "power": 0.0, + "cum_power": 0.0, + "voltage": 0.0, + "vdrop_percent": 0.0, + "distro": { + "in": null, + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.14040007075315172, + 41.69928240812505 + ] + }, + "properties": { + "name": "jamhouse", + "type": "load", + "power": 3000.0, + "cum_power": 3000.0, + "voltage": 99.4, + "vdrop_percent": 56.78, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1399063474789402, + 41.69912586313198 + ] + }, + "properties": { + "name": "distro6", + "type": "load", + "power": 0.0, + "cum_power": 0.0, + "voltage": 0.0, + "vdrop_percent": 0.0, + "distro": { + "in": null, + "out": {} + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/export.geojson.png b/tests/fixtures/export.geojson.png new file mode 100644 index 0000000..45c679d Binary files /dev/null and b/tests/fixtures/export.geojson.png differ diff --git a/tests/fixtures/input_nodes.geojson b/tests/fixtures/input_nodes.geojson index 4485809..fd84795 100644 --- a/tests/fixtures/input_nodes.geojson +++ b/tests/fixtures/input_nodes.geojson @@ -191,9 +191,9 @@ ] }, "properties": { - "name": "Why Not?", - "power": 1840.0, - "type": "load" + "name": "distro example", + "power": 0, + "type": "distro" } }, { @@ -812,4 +812,4 @@ } } ] -} +} \ No newline at end of file diff --git a/tests/fixtures/input_nodes_reduced.geojson b/tests/fixtures/input_nodes_reduced.geojson new file mode 100644 index 0000000..bf68d3d --- /dev/null +++ b/tests/fixtures/input_nodes_reduced.geojson @@ -0,0 +1,320 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.138464932641509, + 41.70066854632285 + ] + }, + "properties": { + "name": "MiddleOfNowhere", + "power": 8300.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137879652831478, + 41.7008596039798 + ] + }, + "properties": { + "name": "NoInfo/Ice", + "power": 2489.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137329960382992, + 41.70109095318956 + ] + }, + "properties": { + "name": "Werkhaus", + "power": 5282.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13665890443399, + 41.70131487330722 + ] + }, + "properties": { + "name": "Cantina (Cantina)", + "power": 4460.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.136354966884593, + 41.700651065766074 + ] + }, + "properties": { + "name": "toolhaus", + "power": 3340.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.136075607508836, + 41.70113479538608 + ] + }, + "properties": { + "name": "production", + "power": 11085.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13709553752102, + 41.70053910616153 + ] + }, + "properties": { + "name": "Kunsthaus", + "power": 3170.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137185016826846, + 41.70085650841904 + ] + }, + "properties": { + "name": "malfare", + "power": 10370.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137889717750607, + 41.70121609813784 + ] + }, + "properties": { + "name": "generator", + "power": 0, + "type": "generator" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137092621102908, + 41.70038656610599 + ] + }, + "properties": { + "name": "Swiss Camp", + "power": 1840.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137791038785941, + 41.70155383234283 + ] + }, + "properties": { + "name": "NIMBY", + "power": 7360.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137967862317445, + 41.70165498843007 + ] + }, + "properties": { + "name": "Jugaton", + "power": 3680.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137499598926543, + 41.701263307068864 + ] + }, + "properties": { + "name": "Why Not?", + "power": 1840.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.136850621282869, + 41.69966999243487 + ] + }, + "properties": { + "name": "It's A Grey Area", + "power": 1840.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137285930416049, + 41.70065526429824 + ] + }, + "properties": { + "name": "Ohana House", + "power": 60.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137001721971925, + 41.700133470839425 + ] + }, + "properties": { + "name": "Le Gratin", + "power": 1840.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137124644449325, + 41.700669066078476 + ] + }, + "properties": { + "name": "welfare (malfare)", + "power": 1730.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137228142727426, + 41.70075879293105 + ] + }, + "properties": { + "name": "guardshack (malfare)", + "power": 480.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137338011431298, + 41.700861693596636 + ] + }, + "properties": { + "name": "meeting point (malfare)", + "power": 250.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.136527711861485, + 41.70121465703139 + ] + }, + "properties": { + "name": "Fridge Truck", + "power": 1000.0, + "type": "load" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13633563273172, + 41.701240803092176 + ] + }, + "properties": { + "name": "Spoonhaus", + "power": 3100.0, + "type": "load" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/nopywer_output_east_grid.geojson b/tests/fixtures/nopywer_output_east_grid.geojson new file mode 100644 index 0000000..31f1cac --- /dev/null +++ b/tests/fixtures/nopywer_output_east_grid.geojson @@ -0,0 +1,1729 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13719101266114425, + 41.69904766985218 + ], + [ + -0.137206218374929, + 41.69953266449073 + ] + ] + }, + "properties": { + "id": "cable_0", + "nodes": [ + "here & meow", + "distro6-1" + ], + "from": "here & meow", + "to": "distro6-1", + "length_m": 63.9, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 17.8, + "cum_power_kw": 3.68, + "vdrop_volts": 17.48 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13784620827857663, + 41.70094177690906 + ], + [ + -0.1377138674373942, + 41.701476697407855 + ] + ] + }, + "properties": { + "id": "cable_1", + "nodes": [ + "distro3", + "dusty dreamers" + ], + "from": "distro3", + "to": "dusty dreamers", + "length_m": 70.4, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 4.8, + "cum_power_kw": 1.0, + "vdrop_volts": 5.23 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1383577949695892, + 41.70111101870848 + ], + [ + -0.13867822792326492, + 41.70159538737178 + ] + ] + }, + "properties": { + "id": "cable_2", + "nodes": [ + "distro5-1", + "camp cambio" + ], + "from": "distro5-1", + "to": "camp cambio", + "length_m": 70.1, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 62.8, + "cum_power_kw": 13.0, + "vdrop_volts": 67.73 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13826592688444675, + 41.701472011492505 + ], + [ + -0.1383577949695892, + 41.70111101870848 + ] + ] + }, + "properties": { + "id": "cable_3", + "nodes": [ + "jugaton", + "distro5-1" + ], + "from": "jugaton", + "to": "distro5-1", + "length_m": 50.8, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 4.8, + "cum_power_kw": 1.0, + "vdrop_volts": 3.77 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13747436010273575, + 41.699116578653026 + ], + [ + -0.137206218374929, + 41.69953266449073 + ] + ] + }, + "properties": { + "id": "cable_4", + "nodes": [ + "hyperflow", + "distro6-1" + ], + "from": "hyperflow", + "to": "distro6-1", + "length_m": 61.3, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 4.8, + "cum_power_kw": 1.0, + "vdrop_volts": 4.56 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13612322506708008, + 41.70063179851722 + ], + [ + -0.13620519471107007, + 41.70082143554413 + ] + ] + }, + "properties": { + "id": "cable_5", + "nodes": [ + "toolhaus", + "production" + ], + "from": "toolhaus", + "to": "production", + "length_m": 32.1, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 16.1, + "cum_power_kw": 3.34, + "vdrop_volts": 7.97 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13784153493839352, + 41.700393705460776 + ], + [ + -0.13777101617447376, + 41.70062032145378 + ] + ] + }, + "properties": { + "id": "cable_6", + "nodes": [ + "distro9", + "noinfo" + ], + "from": "distro9", + "to": "noinfo", + "length_m": 35.9, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 23.9, + "cum_power_kw": 4.94, + "vdrop_volts": 13.18 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13784620827857663, + 41.70094177690906 + ], + [ + -0.13748051264301162, + 41.70142233907045 + ] + ] + }, + "properties": { + "id": "cable_7", + "nodes": [ + "distro3", + "nobarrio" + ], + "from": "distro3", + "to": "nobarrio", + "length_m": 71.5, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 26.7, + "cum_power_kw": 5.52, + "vdrop_volts": 29.34 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13792203842668818, + 41.701452483139114 + ], + [ + -0.13784620827857663, + 41.70094177690906 + ] + ] + }, + "properties": { + "id": "cable_8", + "nodes": [ + "mooncows", + "distro3" + ], + "from": "mooncows", + "to": "distro3", + "length_m": 67.1, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 14.5, + "cum_power_kw": 3.0, + "vdrop_volts": 14.96 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13735634636904792, + 41.70118150860041 + ], + [ + -0.13784620827857663, + 41.70094177690906 + ] + ] + }, + "properties": { + "id": "cable_9", + "nodes": [ + "pink poney paradise", + "distro3" + ], + "from": "pink poney paradise", + "to": "distro3", + "length_m": 58.7, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 4.8, + "cum_power_kw": 1.0, + "vdrop_volts": 4.36 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1374190711291166, + 41.70096791706415 + ], + [ + -0.13784620827857663, + 41.70094177690906 + ] + ] + }, + "properties": { + "id": "cable_10", + "nodes": [ + "le gratin", + "distro3" + ], + "from": "le gratin", + "to": "distro3", + "length_m": 45.7, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 14.5, + "cum_power_kw": 3.0, + "vdrop_volts": 10.19 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1368357235149388, + 41.700207145864105 + ], + [ + -0.13642674451628584, + 41.69994968066739 + ] + ] + }, + "properties": { + "id": "cable_11", + "nodes": [ + "distro4", + "peach please!" + ], + "from": "distro4", + "to": "peach please!", + "length_m": 54.5, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 3.6, + "cum_power_kw": 0.75, + "vdrop_volts": 3.04 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13678209537425187, + 41.70081371865842 + ], + [ + -0.13709073026884433, + 41.70089822126032 + ] + ] + }, + "properties": { + "id": "cable_12", + "nodes": [ + "distro2", + "welfare" + ], + "from": "distro2", + "to": "welfare", + "length_m": 37.4, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 19.3, + "cum_power_kw": 4.0, + "vdrop_volts": 11.12 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13678209537425187, + 41.70081371865842 + ], + [ + -0.1368415609243576, + 41.70105017775877 + ] + ] + }, + "properties": { + "id": "cable_13", + "nodes": [ + "distro2", + "barrio support" + ], + "from": "distro2", + "to": "barrio support", + "length_m": 36.7, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 17.4, + "cum_power_kw": 3.6, + "vdrop_volts": 9.82 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1369738592052911, + 41.700674795566215 + ], + [ + -0.13706825525949862, + 41.70054298992181 + ] + ] + }, + "properties": { + "id": "cable_14", + "nodes": [ + "werkhaus", + "kunsthaus" + ], + "from": "werkhaus", + "to": "kunsthaus", + "length_m": 26.6, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 22.8, + "cum_power_kw": 4.73, + "vdrop_volts": 9.35 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13784153493839352, + 41.700393705460776 + ], + [ + -0.13788338990168958, + 41.70020670287022 + ] + ] + }, + "properties": { + "id": "cable_15", + "nodes": [ + "distro9", + "artinstallation1" + ], + "from": "distro9", + "to": "artinstallation1", + "length_m": 31.1, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 4.8, + "cum_power_kw": 1.0, + "vdrop_volts": 2.31 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13686612235420045, + 41.700910652197706 + ], + [ + -0.13678209537425187, + 41.70081371865842 + ] + ] + }, + "properties": { + "id": "cable_16", + "nodes": [ + "malfare", + "distro2" + ], + "from": "malfare", + "to": "distro2", + "length_m": 22.8, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 33.8, + "cum_power_kw": 7.0, + "vdrop_volts": 11.86 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1368357235149388, + 41.700207145864105 + ], + [ + -0.13648507855198813, + 41.7002150517457 + ] + ] + }, + "properties": { + "id": "cable_17", + "nodes": [ + "distro4", + "ohana haus" + ], + "from": "distro4", + "to": "ohana haus", + "length_m": 39.2, + "area_mm2": 2.5, + "plugs_and_sockets_a": 16.0, + "cable_type": "1P 16A \u2014 2.5mm\u00b2", + "current_a": 0.3, + "cum_power_kw": 0.06, + "vdrop_volts": 0.17 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13784620827857663, + 41.70094177690906 + ], + [ + -0.1383639100113639, + 41.70111117110253 + ] + ] + }, + "properties": { + "id": "cable_18", + "nodes": [ + "distro3", + "distro5-1" + ], + "from": "distro3", + "to": "distro5-1", + "length_m": 57.0, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 62.8, + "cum_power_kw": 13.0, + "vdrop_volts": 22.95 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13711182503054292, + 41.700427091148256 + ], + [ + -0.13729986190314222, + 41.69989841057792 + ] + ] + }, + "properties": { + "id": "cable_19", + "nodes": [ + "distro1", + "distro7" + ], + "from": "distro1", + "to": "distro7", + "length_m": 70.8, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 17.8, + "cum_power_kw": 3.68, + "vdrop_volts": 8.07 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13744814067689898, + 41.70067371329373 + ], + [ + -0.13784153493839352, + 41.700393705460776 + ] + ] + }, + "properties": { + "id": "cable_20", + "nodes": [ + "generator", + "distro9" + ], + "from": "generator", + "to": "distro9", + "length_m": 55.2, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 57.2, + "cum_power_kw": 11.84, + "vdrop_volts": 20.24 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13711247844818722, + 41.70042734787714 + ], + [ + -0.1368357235149388, + 41.700207145864105 + ] + ] + }, + "properties": { + "id": "cable_21", + "nodes": [ + "distro1", + "distro4" + ], + "from": "distro1", + "to": "distro4", + "length_m": 43.6, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 3.9, + "cum_power_kw": 0.81, + "vdrop_volts": 1.09 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1374400580203173, + 41.700680039793006 + ], + [ + -0.13678209537425187, + 41.70081371865842 + ] + ] + }, + "properties": { + "id": "cable_22", + "nodes": [ + "generator", + "distro2" + ], + "from": "generator", + "to": "distro2", + "length_m": 66.8, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 76.1, + "cum_power_kw": 15.74, + "vdrop_volts": 32.57 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13743594305981566, + 41.700675935977124 + ], + [ + -0.13697385920529187, + 41.70067479556621 + ] + ] + }, + "properties": { + "id": "cable_23", + "nodes": [ + "generator", + "werkhaus" + ], + "from": "generator", + "to": "werkhaus", + "length_m": 48.5, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 48.4, + "cum_power_kw": 10.01, + "vdrop_volts": 15.04 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13678209537425187, + 41.70081371865842 + ], + [ + -0.13644600159497342, + 41.70093881235571 + ] + ] + }, + "properties": { + "id": "cable_24", + "nodes": [ + "distro2", + "distro8" + ], + "from": "distro2", + "to": "distro8", + "length_m": 41.2, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 56.7, + "cum_power_kw": 11.75, + "vdrop_volts": 14.99 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.137206218374929, + 41.69953266449073 + ], + [ + -0.13729258524653887, + 41.69988995796666 + ] + ] + }, + "properties": { + "id": "cable_25", + "nodes": [ + "distro6-1", + "distro7" + ], + "from": "distro6-1", + "to": "distro7", + "length_m": 50.3, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 17.8, + "cum_power_kw": 3.68, + "vdrop_volts": 5.73 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13644600159497342, + 41.70093881235571 + ], + [ + -0.13620519471107007, + 41.70082143554413 + ] + ] + }, + "properties": { + "id": "cable_26", + "nodes": [ + "distro8", + "production" + ], + "from": "distro8", + "to": "production", + "length_m": 33.9, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 31.4, + "cum_power_kw": 6.5, + "vdrop_volts": 6.82 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13644600159497342, + 41.70093881235571 + ], + [ + -0.13641071955831735, + 41.70109480960767 + ] + ] + }, + "properties": { + "id": "cable_27", + "nodes": [ + "distro8", + "la cantina" + ], + "from": "distro8", + "to": "la cantina", + "length_m": 27.6, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 25.3, + "cum_power_kw": 5.25, + "vdrop_volts": 4.48 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.13784153493839352, + 41.700393705460776 + ], + [ + -0.13846142429518712, + 41.70047049608174 + ] + ] + }, + "properties": { + "id": "cable_28", + "nodes": [ + "distro9", + "mon" + ], + "from": "distro9", + "to": "mon", + "length_m": 62.3, + "area_mm2": 6.0, + "plugs_and_sockets_a": 32.0, + "cable_type": "1P 32A \u2014 6.0mm\u00b2", + "current_a": 33.3, + "cum_power_kw": 6.9, + "vdrop_volts": 13.31 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1374435252268257, + 41.70067412272905 + ], + [ + -0.13711247844818483, + 41.700427347877145 + ] + ] + }, + "properties": { + "id": "cable_29", + "nodes": [ + "generator", + "distro1" + ], + "from": "generator", + "to": "distro1", + "length_m": 48.9, + "area_mm2": 16.0, + "plugs_and_sockets_a": 63.0, + "cable_type": "1P 63A \u2014 16.0mm\u00b2", + "current_a": 21.4, + "cum_power_kw": 4.43, + "vdrop_volts": 2.52 + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -0.1374471485318656, + 41.70067850485415 + ], + [ + -0.13784620827857663, + 41.70094177690906 + ] + ] + }, + "properties": { + "id": "cable_30", + "nodes": [ + "generator", + "distro3" + ], + "from": "generator", + "to": "distro3", + "length_m": 54.3, + "area_mm2": 16.0, + "plugs_and_sockets_a": 63.0, + "cable_type": "1P 63A \u2014 16.0mm\u00b2", + "current_a": 67.6, + "cum_power_kw": 14.0, + "vdrop_volts": 8.83 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13777101617447376, + 41.70062032145378 + ] + }, + "properties": { + "name": "noinfo", + "type": "load", + "power": 4940.0, + "cum_power": 4940.0, + "voltage": 196.6, + "vdrop_percent": 14.52, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13846142429518712, + 41.70047049608174 + ] + }, + "properties": { + "name": "mon", + "type": "load", + "power": 13800.0, + "cum_power": 13800.0, + "voltage": 196.5, + "vdrop_percent": 14.57, + "distro": { + "in": "3P 32.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13686612235419846, + 41.700910652197706 + ] + }, + "properties": { + "name": "malfare", + "type": "load", + "power": 7000.0, + "cum_power": 7000.0, + "voltage": 185.5, + "vdrop_percent": 19.35, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13706825525949703, + 41.70054298992181 + ] + }, + "properties": { + "name": "kunsthaus", + "type": "load", + "power": 4730.0, + "cum_power": 4730.0, + "voltage": 205.6, + "vdrop_percent": 10.61, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1377138674373942, + 41.701476697407855 + ] + }, + "properties": { + "name": "dusty dreamers", + "type": "load", + "power": 1000.0, + "cum_power": 1000.0, + "voltage": 216.0, + "vdrop_percent": 6.09, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13792203842668818, + 41.701452483139114 + ] + }, + "properties": { + "name": "mooncows", + "type": "load", + "power": 3000.0, + "cum_power": 3000.0, + "voltage": 206.2, + "vdrop_percent": 10.35, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1382660097964005, + 41.70147276635486 + ] + }, + "properties": { + "name": "jugaton", + "type": "load", + "power": 1000.0, + "cum_power": 1000.0, + "voltage": 194.4, + "vdrop_percent": 15.48, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1386777546416652, + 41.70160605466492 + ] + }, + "properties": { + "name": "camp cambio", + "type": "load", + "power": 13000.0, + "cum_power": 13000.0, + "voltage": 130.5, + "vdrop_percent": 43.26, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13641071955831693, + 41.701094809607675 + ] + }, + "properties": { + "name": "la cantina", + "type": "load", + "power": 10490.0, + "cum_power": 10490.0, + "voltage": 177.9, + "vdrop_percent": 22.65, + "distro": { + "in": "3P 32.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13612322506708008, + 41.70063179851722 + ] + }, + "properties": { + "name": "toolhaus", + "type": "load", + "power": 3340.0, + "cum_power": 3340.0, + "voltage": 167.6, + "vdrop_percent": 27.13, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13620519471107007, + 41.70082143554413 + ] + }, + "properties": { + "name": "production", + "type": "load", + "power": 13000.0, + "cum_power": 16340.0, + "voltage": 175.6, + "vdrop_percent": 23.65, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13642674451628584, + 41.69994968066739 + ] + }, + "properties": { + "name": "peach please!", + "type": "load", + "power": 1500.0, + "cum_power": 1500.0, + "voltage": 223.4, + "vdrop_percent": 2.87, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13719101266114425, + 41.69904766985218 + ] + }, + "properties": { + "name": "here & meow", + "type": "load", + "power": 3680.0, + "cum_power": 3680.0, + "voltage": 196.2, + "vdrop_percent": 14.7, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13749597377242917, + 41.69910574225011 + ] + }, + "properties": { + "name": "hyperflow", + "type": "load", + "power": 1000.0, + "cum_power": 1000.0, + "voltage": 209.1, + "vdrop_percent": 9.09, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13744135253153258, + 41.70067734372315 + ] + }, + "properties": { + "name": "generator", + "type": "generator", + "power": 0.0, + "cum_power": 104940.0, + "voltage": 230, + "vdrop_percent": 0.0, + "distro": { + "in": "3P 125A", + "out": { + "3P 32.0A": 3, + "3P 63.0A": 2 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13784153493839313, + 41.70039370546078 + ] + }, + "properties": { + "name": "distro9", + "type": "load", + "power": 0.0, + "cum_power": 19740.0, + "voltage": 209.8, + "vdrop_percent": 8.78, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 2, + "3P 32.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1368357235149388, + 41.700207145864105 + ] + }, + "properties": { + "name": "distro4", + "type": "load", + "power": 0.0, + "cum_power": 1560.0, + "voltage": 226.4, + "vdrop_percent": 1.57, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 2 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13711247844818483, + 41.700427347877145 + ] + }, + "properties": { + "name": "distro1", + "type": "load", + "power": 0.0, + "cum_power": 6240.0, + "voltage": 227.5, + "vdrop_percent": 1.09, + "distro": { + "in": "3P 63.0A", + "out": { + "3P 32.0A": 2 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1383577949695892, + 41.70111101870848 + ] + }, + "properties": { + "name": "distro5-1", + "type": "load", + "power": 0.0, + "cum_power": 14000.0, + "voltage": 198.2, + "vdrop_percent": 13.83, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 2 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13784620827857663, + 41.70094177690906 + ] + }, + "properties": { + "name": "distro3", + "type": "load", + "power": 0.0, + "cum_power": 27520.0, + "voltage": 221.2, + "vdrop_percent": 3.83, + "distro": { + "in": "3P 63.0A", + "out": { + "1P 16.0A": 5, + "3P 32.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13697385920529187, + 41.70067479556621 + ] + }, + "properties": { + "name": "werkhaus", + "type": "load", + "power": 5280.0, + "cum_power": 10010.0, + "voltage": 215.0, + "vdrop_percent": 6.52, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13748051264301162, + 41.70142233907045 + ] + }, + "properties": { + "name": "nobarrio", + "type": "load", + "power": 5520.0, + "cum_power": 5520.0, + "voltage": 191.9, + "vdrop_percent": 16.57, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13735634636904792, + 41.70118150860041 + ] + }, + "properties": { + "name": "pink poney paradise", + "type": "load", + "power": 1000.0, + "cum_power": 1000.0, + "voltage": 216.8, + "vdrop_percent": 5.74, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1374190711291166, + 41.70096791706415 + ] + }, + "properties": { + "name": "le gratin", + "type": "load", + "power": 3000.0, + "cum_power": 3000.0, + "voltage": 211.0, + "vdrop_percent": 8.26, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13644600159497342, + 41.70093881235571 + ] + }, + "properties": { + "name": "distro8", + "type": "load", + "power": 0.0, + "cum_power": 26830.0, + "voltage": 182.4, + "vdrop_percent": 20.7, + "distro": { + "in": "3P 32.0A", + "out": { + "3P 32.0A": 2 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13729986190314222, + 41.69989841057792 + ] + }, + "properties": { + "name": "distro7", + "type": "load", + "power": 0.0, + "cum_power": 4680.0, + "voltage": 219.4, + "vdrop_percent": 4.61, + "distro": { + "in": "3P 32.0A", + "out": { + "3P 32.0A": 1 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.137206218374929, + 41.69953266449073 + ] + }, + "properties": { + "name": "distro6-1", + "type": "load", + "power": 0.0, + "cum_power": 4680.0, + "voltage": 213.7, + "vdrop_percent": 7.09, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 2 + } + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13788338990168839, + 41.70020670287022 + ] + }, + "properties": { + "name": "artinstallation1", + "type": "load", + "power": 1000.0, + "cum_power": 1000.0, + "voltage": 207.5, + "vdrop_percent": 9.78, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13648507855198813, + 41.7002150517457 + ] + }, + "properties": { + "name": "ohana haus", + "type": "load", + "power": 60.0, + "cum_power": 60.0, + "voltage": 226.2, + "vdrop_percent": 1.65, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13709073026884314, + 41.70089822126032 + ] + }, + "properties": { + "name": "welfare", + "type": "load", + "power": 4000.0, + "cum_power": 4000.0, + "voltage": 186.3, + "vdrop_percent": 19.0, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1368415609243572, + 41.70105017775878 + ] + }, + "properties": { + "name": "barrio support", + "type": "load", + "power": 3600.0, + "cum_power": 3600.0, + "voltage": 187.6, + "vdrop_percent": 18.43, + "distro": { + "in": "1P 16.0A", + "out": {} + } + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.13678209537425187, + 41.70081371865842 + ] + }, + "properties": { + "name": "distro2", + "type": "load", + "power": 0.0, + "cum_power": 41430.0, + "voltage": 197.4, + "vdrop_percent": 14.17, + "distro": { + "in": "3P 32.0A", + "out": { + "1P 16.0A": 3, + "3P 32.0A": 1 + } + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/power-nodes.geojson b/tests/fixtures/power-nodes.geojson new file mode 100644 index 0000000..771f9dd --- /dev/null +++ b/tests/fixtures/power-nodes.geojson @@ -0,0 +1,509 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.012601724790615, + 41.69369570931962 + ] + }, + "properties": { + "name": "Smurfberry Square", + "power": 8300.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013187004600646, + 41.69388676697657 + ] + }, + "properties": { + "name": "Frostroot Pond", + "power": 2489.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013736697049132, + 41.69411811618633 + ] + }, + "properties": { + "name": "Windmill Workshop", + "power": 5282.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014407752998134, + 41.694342036303986 + ] + }, + "properties": { + "name": "Bluecap Cantina", + "power": 4460.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014711690547531, + 41.69367822876284 + ] + }, + "properties": { + "name": "Handy Smurf Shed", + "power": 3340.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014991049923288, + 41.69416195838285 + ] + }, + "properties": { + "name": "Sarsaparilla Depot", + "power": 11085.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013971119911104, + 41.693566269158296 + ] + }, + "properties": { + "name": "Painter's Nook", + "power": 3170.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013881640605278, + 41.693883671415804 + ] + }, + "properties": { + "name": "Meadowfare Crossing", + "power": 10370.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013176939681517, + 41.69424326113461 + ] + }, + "properties": { + "name": "Smurf Village Generator", + "power": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013974036329216, + 41.69341372910276 + ] + }, + "properties": { + "name": "Alpine Mushroom Camp", + "power": 1840.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013275618646183, + 41.6945809953396 + ] + }, + "properties": { + "name": "Lantern Courtyard", + "power": 7360.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013098795114679, + 41.69468215142684 + ] + }, + "properties": { + "name": "Jolly Barrel Yard", + "power": 3680.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013567058505581, + 41.69429047006563 + ] + }, + "properties": { + "name": "Whysmurf Way", + "power": 1840.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014216036149255, + 41.69269715543164 + ] + }, + "properties": { + "name": "Greyroot Terrace", + "power": 1840.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013780727016075, + 41.69368242729501 + ] + }, + "properties": { + "name": "Blossom Burrow", + "power": 60.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014064935460199, + 41.69316063383619 + ] + }, + "properties": { + "name": "Gratin Grove", + "power": 1840.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013942012982799, + 41.69369622907524 + ] + }, + "properties": { + "name": "Wellness Wagon", + "power": 1730.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013838514704698, + 41.693785955927815 + ] + }, + "properties": { + "name": "Guardhouse Gate", + "power": 480.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013728646000826, + 41.693888856593404 + ] + }, + "properties": { + "name": "Talking Oak Circle", + "power": 250.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014538945570639, + 41.69424182002816 + ] + }, + "properties": { + "name": "Coldberry Cellar", + "power": 1000.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014731024700404, + 41.694267966088944 + ] + }, + "properties": { + "name": "Spoon Sprout House", + "power": 3100.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014034816911164, + 41.69388206456499 + ] + }, + "properties": { + "name": "Geekstone Workshop", + "power": 2010.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013530550069384, + 41.6939986601976 + ] + }, + "properties": { + "name": "Gloria Garden", + "power": 200.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013177066263024, + 41.69396592012233 + ] + }, + "properties": { + "name": "Mirror Pond", + "power": 1000.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.01277661731886, + 41.693412531888825 + ] + }, + "properties": { + "name": "Veritas Square", + "power": 200.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.012889967974597, + 41.693826743839494 + ] + }, + "properties": { + "name": "Tomorrow Post", + "power": 90.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013052557387299, + 41.69359542301004 + ] + }, + "properties": { + "name": "Moonask Ridge", + "power": 2800.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.012435894206149, + 41.69411427729749 + ] + }, + "properties": { + "name": "Starcap Terrace", + "power": 450.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.012272609024222, + 41.693585054948926 + ] + }, + "properties": { + "name": "Reflection Brook", + "power": 200.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.013614730234054, + 41.69303594213013 + ] + }, + "properties": { + "name": "Split Cottage Lane", + "power": 72.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.011865170446592, + 41.69345512319555 + ] + }, + "properties": { + "name": "Bouncecap Meadow", + "power": 2000.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.012222788859019, + 41.693270366207 + ] + }, + "properties": { + "name": "Lighthouse Knoll", + "power": 300.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014415418961589, + 41.69427829070426 + ] + }, + "properties": { + "name": "Cratewheel Yard", + "power": 0.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.012895850532078, + 41.69472316785931 + ] + }, + "properties": { + "name": "Rainbow Camp", + "power": 3680.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.014002382889214, + 41.69456574065345 + ] + }, + "properties": { + "name": "Soupkettle Corner", + "power": 400.0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0.012336237078207, + 41.694843707438054 + ] + }, + "properties": { + "name": "Dreamdust Field", + "power": 3680.0 + } + } + ] +} \ No newline at end of file diff --git a/tests/test_analyze.py b/tests/test_analyze.py index c7c55f8..13abd9a 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -60,7 +60,7 @@ def test_analyze_raises_when_no_cables_are_present(): analyze(grid) -def test_compute_voltage_drop_uses_phase_voltage_reference(): +def test_ltage_drop_uses_phase_voltage_reference(): generator = PowerNode( name="generator", lon=0.0, diff --git a/uv.lock b/uv.lock index e1bf313..eb3e983 100644 --- a/uv.lock +++ b/uv.lock @@ -14,6 +14,15 @@ conflicts = [[ { package = "nopywer", extra = "pandapower" }, ]] +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -114,6 +123,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + [[package]] name = "fastapi" version = "0.135.1" @@ -203,6 +221,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "immutabledict" +version = "4.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/e6/718471048fea0366c3e3d1df3acfd914ca66d571cdffcf6d37bbcd725708/immutabledict-4.3.1.tar.gz", hash = "sha256:f844a669106cfdc73f47b1a9da003782fb17dc955a54c80972e0d93d1c63c514", size = 7806, upload-time = "2026-02-15T10:32:34.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/ce/f9018bf69ae91b273b6391a095e7c93fa5e1617f25b6ba81ad4b20c9df10/immutabledict-4.3.1-py3-none-any.whl", hash = "sha256:c9facdc0ff30fdb8e35bd16532026cac472a549e182c94fa201b51b25e4bf7bf", size = 5000, upload-time = "2026-02-15T10:32:33.672Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -692,6 +719,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + [[package]] name = "pydantic" version = "2.12.5"