diff --git a/math/Stats.h b/math/Stats.h index b65437b..52101e5 100644 --- a/math/Stats.h +++ b/math/Stats.h @@ -5,5 +5,6 @@ #include "stats/Median.h" #include "stats/Minimum.h" #include "stats/Maximum.h" +#include "stats/Variance.h" #endif // MATH_STATS_H diff --git a/math/stats/Variance.h b/math/stats/Variance.h index 334d2f0..02ec8fe 100644 --- a/math/stats/Variance.h +++ b/math/stats/Variance.h @@ -37,7 +37,7 @@ namespace Stats { /** get the current stadard-deviation */ Scalar getStdDev() const { - return std::sqrt(get); + return std::sqrt(get()); } diff --git a/sensors/radio/VAPGrouper.h b/sensors/radio/VAPGrouper.h index 9a24805..82e728c 100644 --- a/sensors/radio/VAPGrouper.h +++ b/sensors/radio/VAPGrouper.h @@ -38,6 +38,9 @@ public: /** use the maximum signal-strength of all grouped APs */ MAXIMUM, + /** use std-dev around the signal-strength average of all grouped APs. NOTE not directly useful but for debug! */ + STD_DEV, + }; /** how to determine the grouped timestamp */ @@ -62,7 +65,7 @@ private: const Mode mode; /** the signal-strength aggregation algorithm to use */ - const Aggregation agg; + const Aggregation rssiAgg; /** how to aggreage the grouped time */ const TimeAggregation timeAgg; @@ -73,8 +76,8 @@ private: public: /** ctor */ - VAPGrouper(const Mode mode, const Aggregation agg, const TimeAggregation timeAgg = TimeAggregation::AVERAGE, const int minOccurences = 2) : - mode(mode), agg(agg), timeAgg(timeAgg), minOccurences(minOccurences) { + VAPGrouper(const Mode mode, const Aggregation rssiAgg, const TimeAggregation timeAgg = TimeAggregation::AVERAGE, const int minOccurences = 2) : + mode(mode), rssiAgg(rssiAgg), timeAgg(timeAgg), minOccurences(minOccurences) { ; } @@ -98,6 +101,12 @@ public: } + #ifdef WITH_DEBUG_LOG + std::stringstream vals; + vals << std::fixed; + vals.precision(1); + #endif + // to-be-constructed output WiFiMeasurements result; int skipped = 0; @@ -112,19 +121,30 @@ public: if ((int)vaps.size() < minOccurences) {++skipped; continue;} // group all VAPs into one measurement - const WiFiMeasurement groupedMeasurement = groupVAPs(base, vaps); + const WiFiMeasurement groupedMeasurement = groupVAPs(base, vaps, rssiAgg, timeAgg); + + // get corresponding std-dev for debug + #ifdef WITH_DEBUG_LOG + const WiFiMeasurement groupedStdDev = groupVAPs(base, vaps, Aggregation::STD_DEV, timeAgg); + vals << groupedMeasurement.getRSSI() << "±"; + vals << groupedStdDev.getRSSI() << " "; + #endif // add it to the result-vector result.entries.push_back(groupedMeasurement); } + // debug - Log::add(name, + #ifdef WITH_DEBUG_LOG + Log::add(name, "grouped " + std::to_string(original.entries.size()) + " measurements " + - "into " + std::to_string(result.entries.size()) + " [omitted: " + std::to_string(skipped) + "]", + "into " + std::to_string(result.entries.size()) + " [omitted: " + std::to_string(skipped) + "]" + + " Stats:[" + vals.str() + "]", true - ); + ); + #endif // done return result; @@ -135,9 +155,9 @@ public: MACAddress getBaseMAC(const MACAddress& mac) const { switch(mode) { - case Mode::DISABLED: return mac; - case Mode::LAST_MAC_DIGIT_TO_ZERO: return lastMacDigitToZero(mac); - default: throw Exception("unsupported vap-grouping mode given"); + case Mode::DISABLED: return mac; + case Mode::LAST_MAC_DIGIT_TO_ZERO: return lastMacDigitToZero(mac); + default: throw Exception("unsupported vap-grouping mode given"); } } @@ -154,26 +174,24 @@ private: /** combine all of the given VAPs into one entry using the configured aggregation method */ - WiFiMeasurement groupVAPs(const MACAddress& baseMAC, const std::vector& vaps) const { + static WiFiMeasurement groupVAPs(const MACAddress& baseMAC, const std::vector& vaps, Aggregation aggRssi, TimeAggregation aggTime) { // the resulting entry is an AP with the base-MAC all of the given VAPs have in common const AccessPoint baseAP(baseMAC); - // the resultign timestamp - //Timestamp baseTS = vaps.front().getTimestamp(); - // calculate the rssi using the configured aggregate function float rssi = NAN; - switch(agg) { + switch(aggRssi) { case Aggregation::AVERAGE: rssi = getAVG(vaps); break; case Aggregation::MEDIAN: rssi = getMedian(vaps); break; case Aggregation::MAXIMUM: rssi = getMax(vaps); break; + case Aggregation::STD_DEV: rssi = getStdDev(vaps); break; default: throw Exception("unsupported rssi-aggregation method"); } // calculate the time using the configured aggregate function Timestamp baseTS; - switch(timeAgg) { + switch(aggTime) { case TimeAggregation::MINIMUM: baseTS = getMin(vaps); break; case TimeAggregation::AVERAGE: baseTS = getAVG(vaps); break; case TimeAggregation::MAXIMUM: baseTS = getMax(vaps); break; @@ -189,13 +207,8 @@ private: private: /** get the average signal strength */ - template inline T getAVG(const std::vector& vaps) const { + template static inline T getAVG(const std::vector& vaps) { -// T field = T(); -// for (const WiFiMeasurement& vap : vaps) { -// field = field + Field::get(vap); -// } -// return field / vaps.size(); Stats::Average avg; for (const WiFiMeasurement& vap : vaps) { avg.add(Field::get(vap)); @@ -204,8 +217,19 @@ private: } + /** get the std-dev around the average */ + template static inline T getStdDev(const std::vector& vaps) { + + Stats::Variance var; + for (const WiFiMeasurement& vap : vaps) { + var.add(Field::get(vap)); + } + return var.getStdDev(); + + } + /** get the median signal strength */ - inline float getMedian(const std::vector& vaps) const { + static inline float getMedian(const std::vector& vaps) { Stats::Median median; for (const WiFiMeasurement& vap : vaps) { @@ -216,7 +240,7 @@ private: } /** get the maximum value */ - template inline T getMax(const std::vector& vaps) const { + template static inline T getMax(const std::vector& vaps) { Stats::Maximum max; for (const WiFiMeasurement& vap : vaps) { @@ -227,7 +251,7 @@ private: } /** get the minimum value */ - template inline T getMin(const std::vector& vaps) const { + template static inline T getMin(const std::vector& vaps) { Stats::Minimum min; for (const WiFiMeasurement& vap : vaps) { diff --git a/sensors/radio/setup/WiFiFingerprints.h b/sensors/radio/setup/WiFiFingerprints.h index 85da2cf..7d27f70 100644 --- a/sensors/radio/setup/WiFiFingerprints.h +++ b/sensors/radio/setup/WiFiFingerprints.h @@ -13,6 +13,8 @@ */ class WiFiFingerprints { + static constexpr const char* name = "Fingerprints"; + private: // /** the file to save the calibration model to */ @@ -96,6 +98,8 @@ public: } + Log::add(name, "loaded " + std::to_string(fingerprints.size()) + " fingerprints"); + inp.close(); } diff --git a/sensors/radio/setup/WiFiOptimizer.h b/sensors/radio/setup/WiFiOptimizer.h index 125fb7c..97ace2f 100644 --- a/sensors/radio/setup/WiFiOptimizer.h +++ b/sensors/radio/setup/WiFiOptimizer.h @@ -9,6 +9,13 @@ namespace WiFiOptimizer { + enum class Mode { + FAST, + MEDIUM, + QUALITY, + }; + + /** base-class for all WiFiOptimizers */ class Base { diff --git a/sensors/radio/setup/WiFiOptimizerLogDistCeiling.h b/sensors/radio/setup/WiFiOptimizerLogDistCeiling.h index 3ac1c88..837563a 100644 --- a/sensors/radio/setup/WiFiOptimizerLogDistCeiling.h +++ b/sensors/radio/setup/WiFiOptimizerLogDistCeiling.h @@ -10,8 +10,8 @@ #include "WiFiFingerprint.h" #include "WiFiFingerprints.h" -#include "../model/WiFiModel.h" -#include "../model/WiFiModelLogDistCeiling.h" +#include "../model/WiFiModels.h" +//#include "../model/WiFiModelLogDistCeiling.h" #include #include @@ -35,12 +35,6 @@ namespace WiFiOptimizer { public: - enum class Mode { - FAST, - MEDIUM, - QUALITY, - }; - /** * resulting optimization stats for one AP */ @@ -112,6 +106,13 @@ namespace WiFiOptimizer { APParamsMAC(const MACAddress mac, const APParams& params) : mac(mac), params(params) {;} }; + struct OptResultStats { + K::Statistics avgApErrors; // contains one average-error per optimized AP + K::Statistics singleFPErrors; // contains one error for each fingerprint<->ap SIGNED + K::Statistics singleFPErrorsAbs; // contains one error for each fingerprint<->ap ABSOLUTE + + }; + class APParamsList { std::vector lst; @@ -182,12 +183,15 @@ namespace WiFiOptimizer { } /** optimize all known APs */ - APParamsList optimizeAll(APFilter filter) const { + APParamsList optimizeAll(APFilter filter, OptResultStats* dst = nullptr) const { // sanity check Assert::isFalse(getAllMACs().empty(), "no APs found for optimization! call addFingerprint() first!"); - float errSum = 0; int errCnt = 0; + K::Statistics avgErrors; + K::Statistics singleErrors; + K::Statistics singleErrorsAbs; + std::vector res; for (const MACAddress& mac : getAllMACs()) { @@ -198,8 +202,13 @@ namespace WiFiOptimizer { // filter based on stats (option to ignore/filter some access-points) if (!filter(stats, mac)) { res.push_back(APParamsMAC(mac, params)); - errSum += stats.error_db; - ++errCnt; + //errSum += stats.error_db; + //++errCnt; + avgErrors.add(stats.error_db); + for (const auto e : stats.errors) { + singleErrors.add(e.getError_db()); + singleErrorsAbs.add(std::abs(e.getError_db())); + } } else { Log::add(name, "ignoring opt-result for AP " + mac.asString() + " due to filter"); //std::cout << "ignored due to filter!" << std::endl; @@ -207,9 +216,19 @@ namespace WiFiOptimizer { } - const float avgErr = errSum / errCnt; - Log::add(name, "optimized APs: " + std::to_string(errCnt)); - Log::add(name, "average AP error is: " + std::to_string(avgErr) + " dB"); + //const float avgErr = errSum / errCnt; + //Log::add(name, "optimized APs: " + std::to_string(errCnt)); + //Log::add(name, "average AP error is: " + std::to_string(avgErr) + " dB"); + Log::add(name, "optimization result: "); + Log::add(name, " - AvgPerAP " + avgErrors.asString()); + Log::add(name, " - Single: " + singleErrors.asString()); + Log::add(name, " - SingleAbs: " + singleErrorsAbs.asString()); + + if (dst) { + dst->avgApErrors = avgErrors; + dst->singleFPErrors = singleErrors; + dst->singleFPErrorsAbs = singleErrorsAbs; + } // done return APParamsList(res); @@ -237,7 +256,7 @@ namespace WiFiOptimizer { const std::vector valRegion = { LeOpt::MinMax(mapBBox.getMin().x - 20, mapBBox.getMax().x + 20), // x LeOpt::MinMax(mapBBox.getMin().y - 20, mapBBox.getMax().y + 20), // y - LeOpt::MinMax(mapBBox.getMin().z - 5, mapBBox.getMax().z + 5), // z + LeOpt::MinMax(mapBBox.getMin().z - 6, mapBBox.getMax().z + 6), // z LeOpt::MinMax(-50, -30), // txp LeOpt::MinMax(1, 4), // exp LeOpt::MinMax(-15, -0), // waf @@ -258,7 +277,7 @@ namespace WiFiOptimizer { opt.setNumIerations(100); break; case Mode::QUALITY: - opt.setPopulationSize(500); + opt.setPopulationSize(1500); opt.setNumIerations(150); break; } diff --git a/sensors/radio/setup/WiFiOptimizerPerFloor.h b/sensors/radio/setup/WiFiOptimizerPerFloor.h index 0969f56..bd59b6b 100644 --- a/sensors/radio/setup/WiFiOptimizerPerFloor.h +++ b/sensors/radio/setup/WiFiOptimizerPerFloor.h @@ -15,127 +15,186 @@ #include #endif -/** - * uses the log-distance model, but one per floor. - * the model is optimized using all fingerprints that belong to this floor - */ -class WiFiOptimizerPerFloor { +namespace WiFiOptimizer { - WiFiModelPerFloor* mdl = nullptr; + /** + * uses the log-distance model, but one per floor. + * the model is optimized using all fingerprints that belong to this floor + */ + class PerFloor { - WiFiFingerprints fps; + WiFiModelPerFloor* mdl = nullptr; - Floorplan::IndoorMap* map; - std::mutex mtx; + WiFiFingerprints fps; -#ifdef WITH_DEBUG_PLOT - K::Gnuplot gp; - K::GnuplotSplot splot; - K::GnuplotSplotElementColorPoints pts; - K::GnuplotSplotElementLines lines; -#endif + Floorplan::IndoorMap* map; + std::mutex mtx; -public: + VAPGrouper vg; + Mode mode; - WiFiOptimizerPerFloor(Floorplan::IndoorMap* map) : map(map) { + static constexpr const char* name = "WiFiOptFloor"; - // the overall model (contains one sub-model per floor) - mdl = new WiFiModelPerFloor(map); + #ifdef WITH_DEBUG_PLOT + K::Gnuplot gp; + K::GnuplotSplot splot; + K::GnuplotSplotElementColorPoints pts; + K::GnuplotSplotElementLines lines; + #endif - #ifdef WITH_DEBUG_PLOT - splot.add(&pts); pts.setPointSize(1); pts.setPointType(7); - splot.add(&lines); - BBox3 bb = FloorplanHelper::getBBox(map); - splot.getAxisX().setRange(bb.getMin().x, bb.getMax().x); - splot.getAxisY().setRange(bb.getMin().y, bb.getMax().y); - splot.getAxisZ().setRange(bb.getMin().z, bb.getMax().z); - #endif + public: - } + PerFloor(Floorplan::IndoorMap* map, const VAPGrouper& vg, Mode mode) : map(map), vg(vg), mode(mode) { - - /** make the given fingerprint known to the optimizer */ - void addFingerprint(const WiFiFingerprint& fp) { - fps.add(fp); - } - - WiFiModelPerFloor* optimizeAll() { - - const VAPGrouper vg = VAPGrouper(VAPGrouper::Mode::LAST_MAC_DIGIT_TO_ZERO, VAPGrouper::Aggregation::MAXIMUM, VAPGrouper::TimeAggregation::AVERAGE, 1); - - // optimize each floor on its own - //for (WiFiModelPerFloor::ModelForFloor& mdlForFloor : mdl->getFloorModels()) { - for (size_t i = 0; i < map->floors.size(); ++i) { - - const Floorplan::Floor* floor = map->floors[i]; - - // 1) create a new optimizer for the current floor - WiFiOptimizer::LogDistCeiling opt(map, vg); - - // 2) create the model for this floor - mtx.lock(); - WiFiModelLogDistCeiling* mdlForFloor = new WiFiModelLogDistCeiling(map); - mdl->add(mdlForFloor, floor); - mtx.unlock(); - - // 3) get the floor's bbox and adjust the z-region (needed for museum in Rothenburg) - BBox3 bb = FloorplanHelper::getBBox(floor); - bb.setMinZ(floor->atHeight+0.25); - bb.setMaxZ(floor->atHeight+2); - - // 4) find all fingerprints that belong to the floor/model and add them to the optimizer - for (const WiFiFingerprint& fp : fps.getFingerprints()) { - //if (mdlForFloor.matches(fp.pos_m.z)) { - // std::cout << fp.pos_m.z << std::endl; - // opt.addFingerprint(fp); - //} - if (bb.contains(fp.pos_m)) { - //if (fp.pos_m.z >= floor->atHeight && fp.pos_m.z < floor->atHeight+floor->height) { - std::cout << fp.pos_m.z << std::endl; - opt.addFingerprint(fp); - - #ifdef WITH_DEBUG_PLOT - pts.add(K::GnuplotPoint3(fp.pos_m.x, fp.pos_m.y, fp.pos_m.z), i); - #endif - - } - } + // the overall model (contains one sub-model per floor) + mdl = new WiFiModelPerFloor(map); #ifdef WITH_DEBUG_PLOT - for (Floorplan::FloorOutlinePolygon* poly : floor->outline) { - for (const Point2 pt : poly->poly.points) { - lines.add(K::GnuplotPoint3(pt.x, pt.y, floor->atHeight)); - } - lines.splitFace(); lines.splitFace(); - for (const Point2 pt : poly->poly.points) { - lines.add(K::GnuplotPoint3(pt.x, pt.y, floor->atHeight+floor->height)); - } - lines.splitFace(); lines.splitFace(); - } - gp.draw(splot); - gp.flush(); - pts.clear(); - lines.clear(); - sleep(1); + splot.add(&pts); pts.setPointSize(1); pts.setPointType(7); + splot.add(&lines); + BBox3 bb = FloorplanHelper::getBBox(map); + splot.getAxisX().setRange(bb.getMin().x, bb.getMax().x); + splot.getAxisY().setRange(bb.getMin().y, bb.getMax().y); + splot.getAxisZ().setRange(bb.getMin().z, bb.getMax().z); #endif - // 5) run the optimizer - const WiFiOptimizer::LogDistCeiling::APParamsList res = opt.optimizeAll(opt.MIN_2_FPS); - - // 6) add all optimized APs to the floor's model - for (const WiFiOptimizer::LogDistCeiling::APParamsMAC& ap : res.get()) { - // model is per-floor. so model cant optimize waf.. set to VERY HIGH manually - const WiFiModelLogDistCeiling::APEntry entry(ap.params.getPos(), ap.params.txp, ap.params.exp, ap.params.waf); - mdlForFloor->addAP(ap.mac, entry); - } - } - return mdl; - } + /** make the given fingerprint known to the optimizer */ + void addFingerprint(const WiFiFingerprint& fp) { + fps.add(fp); + } -}; + /** add new fingerprints to the optimizer as data-source */ + virtual void addFingerprints(const WiFiFingerprints& fps) { + for (const WiFiFingerprint& fp : fps.getFingerprints()) { + addFingerprint(fp); + } + } + struct OptResultStatsSingle { + K::Statistics avgApErrors; // contains one average-error per optimized AP + K::Statistics singleFPErrors; // contains one error for each fingerprint<->ap SIGNED + K::Statistics singleFPErrorsAbs; // contains one error for each fingerprint<->ap ABSOLUTE + }; + + struct OptResultStats { + OptResultStatsSingle all; + std::vector perFloor; + }; + + WiFiModelPerFloor* optimizeAll(LogDistCeiling::APFilter filter, OptResultStats* dst = nullptr) { + + // be sure we got stats to write to + OptResultStats tmp; + if (!dst) {dst = &tmp;} + + // alloc place for per-floor stats + dst->perFloor.resize(map->floors.size()); + + // optimize each floor on its own + //for (WiFiModelPerFloor::ModelForFloor& mdlForFloor : mdl->getFloorModels()) { + for (size_t i = 0; i < map->floors.size(); ++i) { + + WiFiOptimizer::LogDistCeiling::OptResultStats floorStats; + + const Floorplan::Floor* floor = map->floors[i]; + + // 1) create a new optimizer for the current floor + WiFiOptimizer::LogDistCeiling opt(map, vg, mode); + + // 2) create the model for this floor + mtx.lock(); + WiFiModelLogDistCeiling* mdlForFloor = new WiFiModelLogDistCeiling(map); + mdl->add(mdlForFloor, floor); + mtx.unlock(); + + // 3) get the floor's bbox and adjust the z-region (needed for museum in Rothenburg) + BBox3 bb = FloorplanHelper::getBBox(floor); + bb.setMinZ(floor->atHeight+0.25); + bb.setMaxZ(floor->atHeight+2); + + // 4) find all fingerprints that belong to the floor/model and add them to the optimizer + for (const WiFiFingerprint& fp : fps.getFingerprints()) { + //if (mdlForFloor.matches(fp.pos_m.z)) { + // std::cout << fp.pos_m.z << std::endl; + // opt.addFingerprint(fp); + //} + if (bb.contains(fp.pos_m)) { + //if (fp.pos_m.z >= floor->atHeight && fp.pos_m.z < floor->atHeight+floor->height) { + std::cout << fp.pos_m.z << std::endl; + opt.addFingerprint(fp); + + #ifdef WITH_DEBUG_PLOT + pts.add(K::GnuplotPoint3(fp.pos_m.x, fp.pos_m.y, fp.pos_m.z), i); + #endif + + } + } + + #ifdef WITH_DEBUG_PLOT + for (Floorplan::FloorOutlinePolygon* poly : floor->outline) { + for (const Point2 pt : poly->poly.points) { + lines.add(K::GnuplotPoint3(pt.x, pt.y, floor->atHeight)); + } + lines.splitFace(); lines.splitFace(); + for (const Point2 pt : poly->poly.points) { + lines.add(K::GnuplotPoint3(pt.x, pt.y, floor->atHeight+floor->height)); + } + lines.splitFace(); lines.splitFace(); + } + gp.draw(splot); + gp.flush(); + pts.clear(); + lines.clear(); + sleep(1); + #endif + + // 5) run the optimizer + const WiFiOptimizer::LogDistCeiling::APParamsList res = opt.optimizeAll(filter, &floorStats); + + // 6) add all optimized APs to the floor's model + for (const WiFiOptimizer::LogDistCeiling::APParamsMAC& ap : res.get()) { + // model is per-floor. so model cant optimize waf.. set to VERY HIGH manually + const WiFiModelLogDistCeiling::APEntry entry(ap.params.getPos(), ap.params.txp, ap.params.exp, ap.params.waf); + mdlForFloor->addAP(ap.mac, entry); + } + + // overall stats + dst->all.avgApErrors.add(floorStats.avgApErrors); + dst->all.singleFPErrors.add(floorStats.singleFPErrors); + dst->all.singleFPErrorsAbs.add(floorStats.singleFPErrorsAbs); + + // per-floor stats + dst->perFloor[i].avgApErrors.add(floorStats.avgApErrors); + dst->perFloor[i].singleFPErrors.add(floorStats.singleFPErrors); + dst->perFloor[i].singleFPErrorsAbs.add(floorStats.singleFPErrorsAbs); + + } + + // overall stats + Log::add(name, "optimization result: "); + Log::add(name, " - AvgPerAP " + dst->all.avgApErrors.asString()); + Log::add(name, " - Single: " + dst->all.singleFPErrors.asString()); + Log::add(name, " - SingleAbs: " + dst->all.singleFPErrorsAbs.asString()); + + // per-floor stats + for (size_t i = 0; i < dst->perFloor.size(); ++i) { + Log::add(name, "----------------------------------------------------------------------------------------------------"); + std::string fn = std::to_string(i); + Log::add(name, " - F"+fn+" AvgPerAP " + dst->perFloor[i].avgApErrors.asString()); + Log::add(name, " - F"+fn+" Single: " + dst->perFloor[i].singleFPErrors.asString()); + Log::add(name, " - F"+fn+" SingleAbs: " + dst->perFloor[i].singleFPErrorsAbs.asString()); + + } + + return mdl; + + } + + }; + +} #endif // WIFIOPTIMIZERPERFLOOR_H