diff --git a/src/calibrationtool/graphcompleter/CMakeLists.txt b/src/calibrationtool/graphcompleter/CMakeLists.txt new file mode 100644 index 00000000..b6c687a0 --- /dev/null +++ b/src/calibrationtool/graphcompleter/CMakeLists.txt @@ -0,0 +1,34 @@ +# at the behest of CMake warnings +project(GraphCompleterTest) +cmake_minimum_required(VERSION 3.20) + +# NOTE: to build, run: +# cd +# mkdir build +# cmake -B build +# cmake --build build +# then, run /test/runtests.bat (or a similar batch script that you write yourself for unix platforms and, inmportantly, run from the same directory) to run the tests (and hopefully see no failed ones) + +set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +add_library(graphtestlib + ./src/graphcompleter.hpp + ./src/graphcompleter.cpp) + +target_include_directories(graphtestlib PUBLIC include) + +add_executable(csvtester0 ./test/cpp/csvtest0.cpp) +add_executable(csvtester1 ./test/cpp/csvtest1.cpp) +add_executable(csvtester2 ./test/cpp/csvtest2.cpp) +add_executable(csvtester3 ./test/cpp/csvtest3.cpp) +add_executable(csvtester4 ./test/cpp/csvtest4.cpp) +add_executable(csvtester5 ./test/cpp/csvtest5.cpp) +add_executable(csvtester6 ./test/cpp/csvtest6.cpp) + +target_link_libraries(csvtester0 PRIVATE graphtestlib) +target_link_libraries(csvtester1 PRIVATE graphtestlib) +target_link_libraries(csvtester2 PRIVATE graphtestlib) +target_link_libraries(csvtester3 PRIVATE graphtestlib) +target_link_libraries(csvtester4 PRIVATE graphtestlib) +target_link_libraries(csvtester5 PRIVATE graphtestlib) +target_link_libraries(csvtester6 PRIVATE graphtestlib) \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/README.md b/src/calibrationtool/graphcompleter/README.md new file mode 100644 index 00000000..e2c18da9 --- /dev/null +++ b/src/calibrationtool/graphcompleter/README.md @@ -0,0 +1,37 @@ +# graphcompleter + Solving abstract graph problems for JARVIS. + + Namely, this solves some problems in building an extrinsics inference graph. + + Sometimes, a camera won't have a valid path to the base camera, as in, the number of shared checkerboard frames won't exceed some threshold) + + This often will be a physical limitation of a setup rather than a matter of simply needing more frames. + + The graphcompleter does something similar to [NCams](https://github.com/CMGreenspon/NCams) and creates daisy-chains up to a user-defined maximum depth `k`. + + If it can't find a valid path, it will return the recommended upsampling factors to apply to each camera to achieve a valid graph. For instance, if you only have 20 shared frames between two cameras, if you have a validity threshold of 30 shared frames, the recommended upsampling factor for BOTH cameras will be 1.5. + + Valid paths are found greedily by camera index. In other words, if a camera can daisy-chain through camera 0 with edge weight 30, it will prompty accept that solution and move on, even if camera 12 is over there with edge weight 1 million. As JARVIS resamples cameras non-uniformly, raw shared frame counts beyond the "critical mass" are actually a poor measure of edge quality in any case. + +# Testing notes + Tested in Windows, unfortunately I didn't have access to a linux environment for testing this (my poor laptop does not have the disk space for a VM). Hopefully the only problem that comes of this is that runtests.bat needs to be rewritten for other operating systems. + + To test: + + - `cd /graphcompleter/test/py/` + - `python -m generateTestCases` + - `cd /graphcompleter/` + - `mkdir build` + - `cmake -B build` + - `cmake --build build` + - `runtests` (only if Windows...) + +# Contents + `/src/` contains the source code. Once everything is tested to satisfaction, its files can be included and integrated into the main `calibrationtool` codebase. + + `/test/` contains the test cases, including a Python file located at `/test/py/generateTestCases.py`, which should generate the data required to run the tests when running `python -m generateTestCases`. + + Surface-level directory contains the `CMakeLists` for building the tests and the `runtests.bat` for executing them (on Windows...) + +# Integration notes + Make sure the test cases work (across platforms!) for this siloed version of graphcompleter before integrating! diff --git a/src/calibrationtool/graphcompleter/runtests.bat b/src/calibrationtool/graphcompleter/runtests.bat new file mode 100644 index 00000000..d8be7591 --- /dev/null +++ b/src/calibrationtool/graphcompleter/runtests.bat @@ -0,0 +1,8 @@ +build\Debug\csvtester0.exe +build\Debug\csvtester1.exe +build\Debug\csvtester2.exe +build\Debug\csvtester3.exe +build\Debug\csvtester4.exe +build\Debug\csvtester5.exe +build\Debug\csvtester6.exe +PAUSE \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/src/graphcompleter.cpp b/src/calibrationtool/graphcompleter/src/graphcompleter.cpp new file mode 100644 index 00000000..21087e8a --- /dev/null +++ b/src/calibrationtool/graphcompleter/src/graphcompleter.cpp @@ -0,0 +1,225 @@ +/******************************************************************************* + * File: graphcompleter.cpp + * Created: 22. Aug 2022 + * Author: James Goodman + * Contact: j.mike.goodman@gmail.com + * Copyright: 2022 James Goodman + * License: LGPL v2.1 + ******************************************************************************/ + +#include "graphcompleter.hpp" + +GraphCompleter::GraphCompleter(std::vector *edges, int basecam, int threshold, int maxchain) : +edges(edges), b(basecam), t(threshold), k(maxchain), p(int(edges->size())) +{ + n = nodesFromPairs(); + + try { + testValidGraph(); + } catch (std::invalid_argument &e) { + std::cout << "error: " << e.what() << std::endl; + exit(1); + } +}; + +int GraphCompleter::pairsFromNodes(void) { + return n*(n-1)/2; +} + +int GraphCompleter::nodesFromPairs(void) { + return (1 + int(std::sqrt(1+8*p)))/2; +} + +void GraphCompleter::testValidGraph(void) { + if (pairsFromNodes() != p) { + throw std::invalid_argument("edges vector does not have a valid number of pairs."); + } +} + +int GraphCompleter::oddCeilRoot(int x) { + int res = int(std::sqrt(x)); + + if (int(std::pow(res,2))= p || idx < 0) { + throw std::invalid_argument("index argument to getRow exceeded array extents."); + } + return n - (1 + oddCeilRoot(4*int(std::pow(n,2))-4*n+1-8*idx) )/2; +} + +int GraphCompleter::getCol(int idx, int row) { + if (idx >= p || row >= n || idx < 0 || row < 0) { + throw std::invalid_argument("index or row argument to getCol exceeded array extents."); + } + return idx + row + 1 - row*n + (row*(row+1))/2; +} + + +int GraphCompleter::getIndex(int row_, int col_) { + // row should always be the smaller of the two + std::pair rowcol = std::minmax(row_,col_); + + int &row = rowcol.first; + int &col = rowcol.second; + + if (row >= (n-1) || col >= n || row < 0 || col < 0) { + throw std::invalid_argument("row_ or col_ argument to getIndex exceeded array extents."); + } + return row*n-(row*(row+1))/2+col-row-1; +} + +std::vector> GraphCompleter::getPathsToBase(void) { + return pathsToBase; +} + +std::vector GraphCompleter::getResampleFactors(void) { + return resampleFactors; +} + +float GraphCompleter::getFrameAdd(int edgeIndex) { + // think of "frame add" as: recommended upsampling factor, minus one (will always be zero if edge is above threshold) + if (edges->at(edgeIndex) == 0) { + return std::numeric_limits::max(); // no dividing by zero! + } + if (edges->at(edgeIndex) < t) { + return float(t - edges->at(edgeIndex)) / float(edges->at(edgeIndex)); + } else { + return 0; + } +} + +bool GraphCompleter::completeGraph(void) { + // hierarchical depth progression like Ncams does: O(kp) + // Kruskal's algorithm: O(p log n), but ignores the b and k parameters, which the JARVIS framework would like to give users control over + // (b to permit an intuitive coordinate frame, k to mitigate the instability that daisy-chaining can introduce to extrinsics inference) + // in any case, it's probably safe to assume that k will be on the order of log n anyway + // ergo, hierarchical depth progression it is! + + std::vector> &paths = pathsToBase; + paths.clear(); // idempotence + std::vector pathlens; // in terms of frame adds, not frame counts! + + // start with depth=0 case, wherein you actually first populate paths & pathlens + bool valid = true; + + for (int i = 0; i < n; ++i) { + std::vector path; + float pathlen; + if (b != i) { + int pairIndex = getIndex(b,i); + path.push_back(pairIndex); + pathlen = getFrameAdd(pairIndex); + + if (pathlen > 0) { + valid = false; + } + } else { + pathlen = 0; // the base camera is ground truth! + } + + paths.push_back(path); + pathlens.push_back(pathlen); + } + + // now iterate for each depth until k + int depth = 1; + while (depth < k && !valid) { + valid = true; // set this back to false if you fail to find a valid path for EVERY node + + // save a snapshot of the current state to avoid accidentally testing super long daisy chains as you update state + // there might be a more memory-efficient way to do this + std::vector templens = pathlens; + std::vector> temppaths = paths; + + for (int i = 0; i < (n-1); ++i) { + for (int j = (i+1); j < n; ++j) { + int pairIndex = getIndex(i,j); + float pathleni = templens[j]; + float pathlenj = templens[i]; + float frameadd = getFrameAdd(pairIndex); + + // conditionality so that path lengths are now the sum of nodal resampling factors, rather than merely being assessed at the edges + if (pathleni == 0) { + pathleni += 2*frameadd; // two cameras must now be resampled + } else if ( getFrameAdd(temppaths[j].back()) < frameadd ) { + // new camera must be resampled, and previous one must be further upsampled + pathleni += 2*frameadd; + pathleni -= getFrameAdd(temppaths[j].back()); + } else { + // just the latest camera needs to be upsampled, the previous one has enough new frames to cover it + pathleni += frameadd; + } + + if (pathlenj==0) { + pathlenj += 2*frameadd; + } else if (getFrameAdd(temppaths[i].back()) < frameadd ) { + pathlenj += 2*frameadd; + pathlenj -= getFrameAdd(temppaths[i].back()); + } else { + pathlenj += frameadd; + } + + if (pathleni < pathlens[i]) { + pathlens[i] = pathleni; + paths[i] = temppaths[j]; + paths[i].push_back(pairIndex); + } + + if (pathlenj < pathlens[j]) { + pathlens[j] = pathlenj; + paths[j] = temppaths[i]; + paths[j].push_back(pairIndex); + } + } + + // check to see if you're still on pace to produce a valid tree, or if you still haven't found *the one* + if (pathlens[i]>0) { + valid = false; + } + } + // make sure to check the LAST index, too! (since "i" only iterates up to, but not THROUGH, n-1) + if (pathlens[n-1]>0) { + valid = false; + } + ++depth; + } + + // now determine the upsampling factors you want to use: + + // init (for idempotence) + resampleFactors.clear(); + for (int i = 0; i < paths.size(); ++i) { + resampleFactors.push_back(1); // resampling factor of 1 = change nothing. + } + + // compute + for (int i = 0; i < paths.size(); ++i) { + if (pathlens[i] > 0) { + for (int j=0; jat(paths[i][j]) < t) { + float candidate = 1 + getFrameAdd(paths[i][j]); + int row = getRow(paths[i][j]); + int col = getCol(paths[i][j],row); + + if (resampleFactors[row] < candidate) { + resampleFactors[row] = candidate; + } + + if (resampleFactors[col] < candidate) { + resampleFactors[col] = candidate; + } + } + } + } + } + + // and now return if the path was valid or not + return valid; + +} \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/src/graphcompleter.hpp b/src/calibrationtool/graphcompleter/src/graphcompleter.hpp new file mode 100644 index 00000000..7b2c0fe2 --- /dev/null +++ b/src/calibrationtool/graphcompleter/src/graphcompleter.hpp @@ -0,0 +1,60 @@ +/******************************************************************************* + * File: graphcompleter.hpp + * Created: 22. Aug 2022 + * Author: James Goodman + * Contact: j.mike.goodman@gmail.com + * Copyright: 2022 James Goodman + * License: LGPL v2.1 + ******************************************************************************/ + +// include guard: classic +#ifndef GRAPHCOMPLETER_H +#define GRAPHCOMPLETER_H + +#include // vector, naturally +#include // sqrt and pow +#include // exceptions +#include // minmax, stable_sort +#include // implementation of error messaging +#include // numeric_limits for handling division-by-zero edge cases + +class GraphCompleter { +private: + std::vector *edges; // vector of edge weights. more intuitively represented as a symmetric matrix of shared frame counts between each pair of cameras, this class assumes these data have been converted to the more memory-efficient vector form. + + std::vector> pathsToBase; // for each node, the best path the base camera can take to reach it. + std::vector resampleFactors; // recommended resampling factors for each of the cameras + + int k; // maximum valid length of a daisy chain + int t; // threshold frame count for valid edge + int n; // number of cameras (nodes) + int p; // number of pairs (edges) + int b; // index of baseline camera + + int pairsFromNodes(void); + + int nodesFromPairs(void); + + void testValidGraph(void); + + int oddCeilRoot(int x); + + int getRow(int idx); + + int getCol(int idx, int row); + + int getIndex(int row_, int col_); + + float getFrameAdd(int edgeIndex); + +public: + explicit GraphCompleter(std::vector *edges, int basecam, int threshold, int maxchain); + + std::vector> getPathsToBase(void); + + std::vector getResampleFactors(void); + + bool completeGraph(void); +}; + +#endif \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/test/cpp/csvtest0.cpp b/src/calibrationtool/graphcompleter/test/cpp/csvtest0.cpp new file mode 100644 index 00000000..6c2ad741 --- /dev/null +++ b/src/calibrationtool/graphcompleter/test/cpp/csvtest0.cpp @@ -0,0 +1,76 @@ +#include "../../src/graphcompleter.hpp" +#include +#include +#include + +int main() { + std::string fname = "./test/csv/testCase0.csv"; + + // some params + int threshold = 30; + int maxchain = 2; + + std::string line, word; + + std::fstream file (fname, std::ios::in); + + int testCaseIndex = 0; + int numFailedCases = 0; + + if(file.is_open()) { + while(std::getline(file, line)) { + bool collectEdges = false; + bool getBase = false; + int basecam; + std::vector edges; + + std::stringstream str(line); + + while(std::getline(str, word, '\t')) { + if (collectEdges) { + edges.push_back(std::stoi(word)); + } + + if (getBase) { + basecam = std::stoi(word); + getBase = false; + } + + if (word.compare("baseCam") == 0) { + getBase = true; + } + + if (word.compare("pairCounts") == 0) { + collectEdges = true; + } + } + + GraphCompleter gc(&edges,basecam,threshold,maxchain); + bool validGraph = gc.completeGraph(); + + // std::cout << "======================================================================================================" << std::endl; + // std::cout << "Test case " << testCaseIndex; + + if (validGraph) { + // std::cout << " passed"; + } else { + // std::cout << " failed"; + ++numFailedCases; + } + + ++testCaseIndex; + } + } else { + std::cout << "======================================================================================================" << std::endl; + std::cout << "Test cases for set 0 failed: "; + std::cout << "Could not open the file" << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + return 1; + } + + std::cout << "======================================================================================================" << std::endl; + std::cout << "Number of failed test cases for set 0: " << numFailedCases << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + + return 0; +} \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/test/cpp/csvtest1.cpp b/src/calibrationtool/graphcompleter/test/cpp/csvtest1.cpp new file mode 100644 index 00000000..4f8bbc54 --- /dev/null +++ b/src/calibrationtool/graphcompleter/test/cpp/csvtest1.cpp @@ -0,0 +1,90 @@ +#include "../../src/graphcompleter.hpp" +#include +#include +#include + +int main() { + std::string fname = "./test/csv/testCase1.csv"; + + // some params + int threshold = 30; + int maxchain = 2; + + std::string line, word; + + std::fstream file (fname, std::ios::in); + + int testCaseIndex = 0; + int numFailedCases = 0; + + if(file.is_open()) { + while(std::getline(file, line)) { + bool collectEdges = false; + bool getBase = false; + bool getDaisyChainer = false; + int basecam; + int daisyChainer; + std::vector edges; + + std::stringstream str(line); + + while(std::getline(str, word, '\t')) { + if (collectEdges) { + edges.push_back(std::stoi(word)); + } + + if (getBase) { + basecam = std::stoi(word); + getBase = false; + } + + if (getDaisyChainer) { + daisyChainer = std::stoi(word); + getDaisyChainer = false; + } + + if (word.compare("baseCam") == 0) { + getBase = true; + } + + if (word.compare("pairCounts") == 0) { + collectEdges = true; + } + + if (word.compare("daisyChainer") == 0) { + getDaisyChainer = true; + } + } + + // next, test the graph completer + GraphCompleter gc(&edges,basecam,threshold,maxchain); + bool validGraph = gc.completeGraph(); + std::vector> pathsToBase = gc.getPathsToBase(); + std::vector resampleFactors = gc.getResampleFactors(); + + // std::cout << "======================================================================================================" << std::endl; + // std::cout << "Test case " << testCaseIndex; + + if (validGraph && pathsToBase[daisyChainer].size() > 1) { + // std::cout << " passed"; + } else { + // std::cout << " failed"; + ++numFailedCases; + } + + ++testCaseIndex; + } + } else { + std::cout << "======================================================================================================" << std::endl; + std::cout << "Test cases for set 1 failed: "; + std::cout << "Could not open the file" << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + return 1; + } + + std::cout << "======================================================================================================" << std::endl; + std::cout << "Number of failed test cases for set 1: " << numFailedCases << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + + return 0; +} \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/test/cpp/csvtest2.cpp b/src/calibrationtool/graphcompleter/test/cpp/csvtest2.cpp new file mode 100644 index 00000000..ae5a841d --- /dev/null +++ b/src/calibrationtool/graphcompleter/test/cpp/csvtest2.cpp @@ -0,0 +1,98 @@ +#include "../../src/graphcompleter.hpp" +#include +#include +#include + +int main() { + std::string fname = "./test/csv/testCase2.csv"; + + // some params + int threshold = 30; + int maxchain = 2; + + std::string line, word; + + std::fstream file (fname, std::ios::in); + + int testCaseIndex = 0; + int numFailedCases = 0; + + if(file.is_open()) { + while(std::getline(file, line)) { + bool collectEdges = false; + bool getBase = false; + bool getDaisyChainer = false; + int basecam; + std::vector daisyChainers; + std::vector edges; + + std::stringstream str(line); + + while(std::getline(str, word, '\t')) { + if (collectEdges) { + edges.push_back(std::stoi(word)); + } + + if (getBase) { + basecam = std::stoi(word); + getBase = false; + } + + // weird order, but meant to prevent getDaisyChainer from processing text instead of numerals + if (word.compare("pairCounts") == 0) { + collectEdges = true; + getDaisyChainer = false; + } + + if (getDaisyChainer) { + daisyChainers.push_back(std::stoi(word)); + } + + if (word.compare("baseCam") == 0) { + getBase = true; + } + + if (word.compare("daisyChainers") == 0) { + getDaisyChainer = true; + } + } + + // next, test the graph completer + GraphCompleter gc(&edges,basecam,threshold,maxchain); + bool validGraph = gc.completeGraph(); + std::vector> pathsToBase = gc.getPathsToBase(); + std::vector resampleFactors = gc.getResampleFactors(); + + // std::cout << "======================================================================================================" << std::endl; + // std::cout << "Test case " << testCaseIndex; + + int whichChainer = 0; + while (validGraph && whichChainer < daisyChainers.size()) { + int thisChainer = daisyChainers[whichChainer]; + validGraph = pathsToBase[thisChainer].size() > 1; + ++whichChainer; + } + + if (validGraph) { + // std::cout << " passed"; + } else { + // std::cout << " failed"; + ++numFailedCases; + } + + ++testCaseIndex; + } + } else { + std::cout << "======================================================================================================" << std::endl; + std::cout << "Test cases for set 2 failed: "; + std::cout << "Could not open the file" << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + return 1; + } + + std::cout << "======================================================================================================" << std::endl; + std::cout << "Number of failed test cases for set 2: " << numFailedCases << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + + return 0; +} \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/test/cpp/csvtest3.cpp b/src/calibrationtool/graphcompleter/test/cpp/csvtest3.cpp new file mode 100644 index 00000000..76960490 --- /dev/null +++ b/src/calibrationtool/graphcompleter/test/cpp/csvtest3.cpp @@ -0,0 +1,108 @@ +#include "../../src/graphcompleter.hpp" +#include +#include +#include + +int main() { + std::string fname = "./test/csv/testCase3.csv"; + + // some params + int threshold = 30; + int maxchain; + + std::string line, word; + + std::fstream file (fname, std::ios::in); + + int testCaseIndex = 0; + int numFailedCases = 0; + + if(file.is_open()) { + while(std::getline(file, line)) { + bool collectEdges = false; + bool getBase = false; + bool getDaisyChainer = false; + bool getCamCount = false; + int basecam; + std::vector daisyChainers; + std::vector edges; + + std::stringstream str(line); + + while(std::getline(str, word, '\t')) { + if (collectEdges) { + edges.push_back(std::stoi(word)); + } + + if (getBase) { + basecam = std::stoi(word); + getBase = false; + } + + if (getCamCount) { + maxchain = std::stoi(word) / 2 + 2; // consistent with the test case parameters + getCamCount = false; + } + + // weird order, but meant to prevent getDaisyChainer from processing text instead of numerals + if (word.compare("pairCounts") == 0) { + collectEdges = true; + getDaisyChainer = false; + } + + if (getDaisyChainer) { + daisyChainers.push_back(std::stoi(word)); + } + + if (word.compare("baseCam") == 0) { + getBase = true; + } + + if (word.compare("longDaisyChainers") == 0) { + getDaisyChainer = true; + } + + if (word.compare("nCams") == 0) { + getCamCount = true; + } + } + + // next, test the graph completer + GraphCompleter gc(&edges,basecam,threshold,maxchain); + bool validGraph = gc.completeGraph(); + std::vector> pathsToBase = gc.getPathsToBase(); + std::vector resampleFactors = gc.getResampleFactors(); + + // std::cout << "======================================================================================================" << std::endl; + // std::cout << "Test case " << testCaseIndex; + + int whichChainer = 1; // index 0 has direct base connection + while (validGraph && whichChainer < daisyChainers.size()) { + int thisChainer = daisyChainers[whichChainer]; + validGraph = pathsToBase[thisChainer].size() > 1; + ++whichChainer; + } + + if (validGraph) { + // std::cout << " passed"; + } else { + // std::cout << " failed"; + ++numFailedCases; + } + + ++testCaseIndex; + } + } else { + std::cout << "======================================================================================================" << std::endl; + std::cout << "Test cases for set 3 failed: "; + std::cout << "Could not open the file" << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + return 1; + } + + std::cout << "======================================================================================================" << std::endl; + std::cout << "Number of failed test cases for set 3: " << numFailedCases << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + + return 0; +} \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/test/cpp/csvtest4.cpp b/src/calibrationtool/graphcompleter/test/cpp/csvtest4.cpp new file mode 100644 index 00000000..de2798fb --- /dev/null +++ b/src/calibrationtool/graphcompleter/test/cpp/csvtest4.cpp @@ -0,0 +1,135 @@ +#include "../../src/graphcompleter.hpp" +#include +#include +#include +#include + +int main() { + std::string fname = "./test/csv/testCase4.csv"; + + // some params + int threshold = 30; + int maxchain; + + std::string line, word; + + std::fstream file (fname, std::ios::in); + + int testCaseIndex = 0; + int numFailedCases = 0; + + if(file.is_open()) { + while(std::getline(file, line)) { + bool collectEdges = false; + bool getBase = false; + bool getDaisyChainer = false; + bool getCamCount = false; + int basecam; + std::vector> daisyChainers; + std::vector tempChainers; + std::vector edges; + + std::stringstream str(line); + + while(std::getline(str, word, '\t')) { + // compare function returns 0 for "true", otherwise it gives a little info about how they don't match with a nonzero signed int + if (collectEdges) { + edges.push_back(std::stoi(word)); + } + + if (getBase) { + basecam = std::stoi(word); + getBase = false; + } + + if (getCamCount) { + maxchain = std::stoi(word) / 2 + 2; // consistent with the test case parameters + getCamCount = false; + } + + // weird order, but meant to prevent getDaisyChainer from processing text instead of numerals + if (word.compare("pairCounts") == 0) { + collectEdges = true; + getDaisyChainer = false; + daisyChainers.push_back(tempChainers); // don't skip the last one! + } + + if (word.compare("longDaisyChainers") == 0) { + getDaisyChainer = true; + if (tempChainers.size() > 0) { + daisyChainers.push_back(tempChainers); + } + tempChainers.clear(); + continue; // messy control statement use but helps to prevent getDaisyChainer from processing text. In retrospect, a switch-case structure or else if structure would've worked better. + } + + if (getDaisyChainer) { + tempChainers.push_back(std::stoi(word)); + } + + if (word.compare("baseCam") == 0) { + getBase = true; + } + + if (word.compare("nCams") == 0) { + getCamCount = true; + } + } + + // next, test the graph completer + GraphCompleter gc(&edges,basecam,threshold,maxchain); + bool validGraph = gc.completeGraph(); + std::vector> pathsToBase = gc.getPathsToBase(); + std::vector resampleFactors = gc.getResampleFactors(); + + // std::cout << "======================================================================================================" << std::endl; + // std::cout << "Test case " << testCaseIndex; + + // let's make a lookup table of all base-connectors + std::unordered_set baseConnectors; + + for (const auto &chain : daisyChainers) { + baseConnectors.insert( chain[0] ); + } + + std::unordered_set longChainers; + + // now run through all chains, make sure all of the fellas that aren't a member of baseConnectors are ending up with larger-than-1 chains + int whichChain = 0; + while (validGraph && whichChain < daisyChainers.size()) { + std::vector &chain = daisyChainers[whichChain]; + int whichChainer = 1; // ignore the first index, you already processed them + while (validGraph && whichChainer < chain.size()) { + int &chainer = chain[whichChainer]; + if (baseConnectors.count(chainer) == 0) { + validGraph = pathsToBase[chainer].size() > 1; + longChainers.insert(chainer); + } + ++whichChainer; + } + ++whichChain; + } + + if (validGraph) { + // std::cout << " passed"; + } else { + // std::cout << " failed"; + ++numFailedCases; + } + + ++testCaseIndex; + } + } else { + std::cout << "======================================================================================================" << std::endl; + std::cout << "Test cases for set 4 failed: "; + std::cout << "Could not open the file" << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + return 1; + } + + std::cout << "======================================================================================================" << std::endl; + std::cout << "Number of failed test cases for set 4: " << numFailedCases << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + + return 0; +} \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/test/cpp/csvtest5.cpp b/src/calibrationtool/graphcompleter/test/cpp/csvtest5.cpp new file mode 100644 index 00000000..4e3ca26b --- /dev/null +++ b/src/calibrationtool/graphcompleter/test/cpp/csvtest5.cpp @@ -0,0 +1,103 @@ +#include "../../src/graphcompleter.hpp" +#include +#include +#include + +int main() { + std::string fname = "./test/csv/testCase5.csv"; + + // some params + int threshold = 30; + int maxchain = 2; + + std::string line, word; + + std::fstream file (fname, std::ios::in); + + int testCaseIndex = 0; + int numFailedCases = 0; + + if(file.is_open()) { + while(std::getline(file, line)) { + bool collectEdges = false; + bool getBase = false; + bool getIslander = false; + int basecam; + int islander; + std::vector edges; + + std::stringstream str(line); + + while(std::getline(str, word, '\t')) { + if (collectEdges) { + edges.push_back(std::stoi(word)); + } + + if (getBase) { + basecam = std::stoi(word); + getBase = false; + } + + if (getIslander) { + islander = std::stoi(word); + getIslander = false; + } + + if (word.compare("baseCam") == 0) { + getBase = true; + } + + if (word.compare("pairCounts") == 0) { + collectEdges = true; + } + + if (word.compare("islander") == 0) { + getIslander = true; + } + } + + // next, test the graph completer + GraphCompleter gc(&edges,basecam,threshold,maxchain); + bool validGraph = gc.completeGraph(); + std::vector> pathsToBase = gc.getPathsToBase(); + std::vector resampleFactors = gc.getResampleFactors(); + + // std::cout << "======================================================================================================" << std::endl; + // std::cout << "Test case " << testCaseIndex; + + // check all other fellas to see if they have a valid path + bool validExceptIslander = validGraph; + + for (int idx=0; idx < resampleFactors.size(); ++idx){ + if (idx == islander || idx == basecam) { + continue; + } + validExceptIslander = resampleFactors[idx] == 1; + if (!validExceptIslander){ + break; + } + } + + if (!validGraph && validExceptIslander && resampleFactors[islander] > 1) { + // std::cout << " passed"; + } else { + // std::cout << " failed"; + ++numFailedCases; + } + + ++testCaseIndex; + } + } else { + std::cout << "======================================================================================================" << std::endl; + std::cout << "Test cases for set 5 failed: "; + std::cout << "Could not open the file" << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + return 1; + } + + std::cout << "======================================================================================================" << std::endl; + std::cout << "Number of failed test cases for set 5: " << numFailedCases << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + + return 0; +} \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/test/cpp/csvtest6.cpp b/src/calibrationtool/graphcompleter/test/cpp/csvtest6.cpp new file mode 100644 index 00000000..104eef22 --- /dev/null +++ b/src/calibrationtool/graphcompleter/test/cpp/csvtest6.cpp @@ -0,0 +1,109 @@ +#include "../../src/graphcompleter.hpp" +#include +#include +#include +#include + +int main() { + std::string fname = "./test/csv/testCase6.csv"; + + // some params + int threshold = 30; + int maxchain = 2; + + std::string line, word; + + std::fstream file (fname, std::ios::in); + + int testCaseIndex = 0; + int numFailedCases = 0; + + if(file.is_open()) { + while(std::getline(file, line)) { + bool collectEdges = false; + bool getBase = false; + bool getIslander = false; + int basecam; + std::unordered_set islanders; + std::vector edges; + + std::stringstream str(line); + + while(std::getline(str, word, '\t')) { + // compare function returns 0 for "true", otherwise it gives a little info about how they don't match with a nonzero signed int + if (collectEdges) { + edges.push_back(std::stoi(word)); + } + + if (getBase) { + basecam = std::stoi(word); + getBase = false; + } + + // weird order, but meant to prevent getIslander from processing text instead of numerals + if (word.compare("pairCounts") == 0) { + collectEdges = true; + getIslander = false; + } + + if (getIslander) { + islanders.insert(std::stoi(word)); + } + + if (word.compare("baseCam") == 0) { + getBase = true; + } + + if (word.compare("islanders") == 0) { + getIslander = true; + } + } + + // next, test the graph completer + GraphCompleter gc(&edges,basecam,threshold,maxchain); + bool validGraph = gc.completeGraph(); + std::vector> pathsToBase = gc.getPathsToBase(); + std::vector resampleFactors = gc.getResampleFactors(); + + // std::cout << "======================================================================================================" << std::endl; + // std::cout << "Test case " << testCaseIndex; + + // check all other fellas to see if they have a valid path + bool validExceptIslanders = true; + + for (int idx=0; idx < resampleFactors.size(); ++idx){ + if (islanders.count(idx)==1) { + validExceptIslanders = resampleFactors[idx] > 1; + } else if (idx==basecam) { + continue; + } else { + validExceptIslanders = resampleFactors[idx] == 1; + } + if (!validExceptIslanders){ + break; + } + } + + if (!validGraph && validExceptIslanders) { + // std::cout << " passed"; + } else { + // std::cout << " failed"; + ++numFailedCases; + } + + ++testCaseIndex; + } + } else { + std::cout << "======================================================================================================" << std::endl; + std::cout << "Test cases for set 6 failed: "; + std::cout << "Could not open the file" << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + return 1; + } + + std::cout << "======================================================================================================" << std::endl; + std::cout << "Number of failed test cases for set 6: " << numFailedCases << std::endl; + std::cout << "======================================================================================================" << std::endl << std::endl; + + return 0; +} \ No newline at end of file diff --git a/src/calibrationtool/graphcompleter/test/py/generateTestCases.py b/src/calibrationtool/graphcompleter/test/py/generateTestCases.py new file mode 100644 index 00000000..4e4bbe42 --- /dev/null +++ b/src/calibrationtool/graphcompleter/test/py/generateTestCases.py @@ -0,0 +1,411 @@ +# whip up some test cases for your graph solution in c++ +# case set 0: everyone has a valid path to the base camera +# case set 1: one guy has to daisy chain through 1 node to reach a valid path +# case set 2: multiple one-long chains +# case set 3: one long daisy-chain +# case set 4: multiple long daisy-chains +# case set 5: one guy simply does not have a valid path to the base +# case set 6: multiple guys lack a valid path to the base + +from random import randrange, shuffle +# from math import comb +from scipy.special import comb # for compatibility with older pythons +import os +import csv +from math import ceil, sqrt + +# pandas dataframes to make for nice metadata? + +minCams = 4 +maxCams = 128 + +mink = 2 +maxk = 10 + +Niter = 100 + +threshold = 30 +maxval = 100 + +# %% +# constructor 0: everyone has a valid path +fldr = os.path.join('..','csv') +if not os.path.exists(fldr): + os.mkdir(fldr) + +csvfile = os.path.join('..','csv','testCase0.csv') + +# cleanup +with open(csvfile,"w+") as _: + pass + +for _ in range(Niter): + nCams = randrange(minCams,maxCams+1) + baseCam = randrange(0,nCams) + + # we frankly don't care about thresholds here, we'll just make a bunch of values and set the important ones to be above threshold later + veclen = nCams*(nCams-1) // 2 + vec = [randrange(maxval+1) for _ in range(veclen)] + + # now for each guy connected to the basecam: + for newCam in range(nCams): + if newCam == baseCam: + pass + else: + i = min(newCam,baseCam) + j = max(newCam,baseCam) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) # https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.squareform.html + + vec[idx] = randrange(threshold,maxval) + + with open(csvfile,"a") as f: + wrobj = csv.writer(f,delimiter='\t') + vec = ["nCams",nCams,"baseCam",baseCam,"pairCounts"] + vec + wrobj.writerow(vec) + +# %% +# constructor 1: one fella needs to daisy chain +csvfile = os.path.join('..','csv','testCase1.csv') + +# cleanup +with open(csvfile,"w+") as _: + pass + +for _ in range(Niter): + nCams = randrange(minCams,maxCams+1) + baseCam = randrange(0,nCams) + + veclen = nCams*(nCams-1) // 2 + vec = [randrange(maxval+1) for _ in range(veclen)] + + daisyChainer = randrange(nCams-1) + daisyChainer = daisyChainer+1 if daisyChainer>=baseCam else daisyChainer + for newCam in range(nCams): + if newCam == baseCam: + pass + else: + i = min(newCam,baseCam) + j = max(newCam,baseCam) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + if newCam == daisyChainer: + vec[idx] = randrange(0,threshold) + + # also pick a random index to chain through + chainIdx = randrange(nCams-2) + chainIdx = chainIdx+1 if chainIdx >= min(daisyChainer,baseCam) else chainIdx + chainIdx = chainIdx+1 if chainIdx >= max(daisyChainer,baseCam) else chainIdx + i = min(chainIdx,daisyChainer) + j = max(chainIdx,daisyChainer) + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + vec[idx] = randrange(ceil(sqrt(2)*threshold),maxval+1) + else: + vec[idx] = randrange(ceil(sqrt(2)*threshold),maxval+1) + + with open(csvfile,"a") as f: + wrobj = csv.writer(f,delimiter='\t') + vec = ["nCams",nCams,"baseCam",baseCam,"daisyChainer",daisyChainer,"pairCounts"] + vec + wrobj.writerow(vec) + +# %% +# constructor 2: several fellas need to daisy chain (still producing the occasional failed case...) +csvfile = os.path.join('..','csv','testCase2.csv') + +# cleanup +with open(csvfile,"w+") as _: + pass + +for _ in range(Niter): + nCams = randrange(minCams,maxCams+1) + baseCam = randrange(0,nCams) + + veclen = nCams*(nCams-1) // 2 + vec = [randrange(maxval+1) for _ in range(veclen)] + + nDaisyChainers = randrange(2,nCams//2+1) + daisyChainers = [i for i in range(nCams-1)] + shuffle(daisyChainers) + daisyChainers = daisyChainers[:nDaisyChainers] + daisyChainers = [daisyChainer+1 if daisyChainer>=baseCam else daisyChainer for daisyChainer in daisyChainers] + + for newCam in range(nCams): + if newCam == baseCam: + pass + else: + i = min(newCam,baseCam) + j = max(newCam,baseCam) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + if newCam in daisyChainers: + vec[idx] = randrange(0,threshold) + + # also pick a random non-chaining index to chain through + chainIdx = randrange(nCams-len(daisyChainers)-1) + + dc = daisyChainers + [baseCam] + dc.sort() + for chainer in dc: + chainIdx = chainIdx+1 if chainIdx >= chainer else chainIdx + i = min(chainIdx,newCam) + j = max(chainIdx,newCam) + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + vec[idx] = randrange(ceil(sqrt(2)*threshold),maxval+1) + else: + vec[idx] = randrange(ceil(sqrt(2)*threshold),maxval+1) # guarantees that non-daisychainers have a connection to the base cam. + + with open(csvfile,"a") as f: + wrobj = csv.writer(f,delimiter='\t') + vec = ["nCams",nCams,"baseCam",baseCam,"daisyChainers"] + daisyChainers + ["pairCounts"] + vec + wrobj.writerow(vec) + +# %% +# constructor 3: the existence of a single multi-chain +csvfile = os.path.join('..','csv','testCase3.csv') + +# cleanup +with open(csvfile,"w+") as _: + pass + +for iter_ in range(Niter): + nCams = randrange(minCams,maxCams+1) + baseCam = randrange(0,nCams) + + veclen = nCams*(nCams-1) // 2 + vec = [randrange(maxval+1) for _ in range(veclen)] + + nDaisyChainers = randrange(3,nCams//2+2) + daisyChainers = [i for i in range(nCams-1)] + shuffle(daisyChainers) + daisyChainers = daisyChainers[:nDaisyChainers] + daisyChainers = [daisyChainer+1 if daisyChainer>=baseCam else daisyChainer for daisyChainer in daisyChainers] + temp = daisyChainers[:] + + for newCam in range(nCams): + # don't exclude baseCam from newCam this time... + for chainer in daisyChainers[1:]: # sever all connections with the daisy chainers (except perhaps the last one) + # note: this is inefficient, I'm running through a bunch of pairs twice. + + # first of all, only do this if the chainer is not your newCam + if newCam != chainer: + i = min(newCam,chainer) + j = max(newCam,chainer) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + vec[idx] = randrange(0,threshold) + + # also make sure to guarantee this newcam connects to the base (if it isn't already the base, and isn't a member of the chainers that have no business connecting to the base) + if newCam != baseCam and newCam not in daisyChainers[1:]: + i = min(newCam,baseCam) + j = max(newCam,baseCam) + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + vec[idx] = randrange(threshold,maxval+1) + + # now run thru the daisychain! + while len(daisyChainers) > 0: + chainer = daisyChainers.pop() + if len(daisyChainers)==0: # inefficient... + i = min(baseCam,chainer) # the last guy in the chain really ought to connect to the base... + j = max(baseCam,chainer) + else: + i = min(daisyChainers[-1],chainer) + j = max(daisyChainers[-1],chainer) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + vec[idx] = randrange(ceil( threshold*sqrt(nDaisyChainers) ),ceil( maxval*sqrt(nDaisyChainers) )+1) # make sure the entries among the daisy chainers actually connect... + + with open(csvfile,"a") as f: + wrobj = csv.writer(f,delimiter='\t') + vec = ["nCams",nCams,"baseCam",baseCam,"longDaisyChainers"] + temp + ["pairCounts"] + vec + wrobj.writerow(vec) + +# %% +# constructor 4: the existence of several multi-chains +csvfile = os.path.join('..','csv','testCase4.csv') + +# cleanup +with open(csvfile,"w+") as _: + pass + +for iter_ in range(Niter): + nCams = randrange(minCams,maxCams+1) + nChains = randrange(2,nCams//2+1) + baseCam = randrange(0,nCams) + + veclen = nCams*(nCams-1) // 2 + vec = [randrange(maxval+1) for _ in range(veclen)] + + daisyChainers = [] + for _ in range(nChains): + nDaisyChainers = randrange(3,nCams//2+2) + dc = [i for i in range(nCams-1)] + shuffle(dc) + dc = dc[:nDaisyChainers] + dc = [dc_+1 if dc_>=baseCam else dc_ for dc_ in dc] + daisyChainers += [dc] + + allDaisyChainers = [] + for dc in daisyChainers: + allDaisyChainers += dc[1:] # exclude the final nodes + + temp = [] + for chain in daisyChainers: + temp += [chain[:]] + + for newCam in range(nCams): + # don't exclude baseCam from newCam this time... + for chainer in allDaisyChainers: + + # first of all, only do this if the chainer is not your newCam + if newCam != chainer: + i = min(newCam,chainer) + j = max(newCam,chainer) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + vec[idx] = randrange(0,threshold) + + # also make sure to guarantee this newcam connects to the base (if it isn't already the base, and isn't a member of the chainers which have no business connecting to the base, except the last one but we reset its value later anyway) + if newCam != baseCam and newCam not in allDaisyChainers: + i = min(newCam,baseCam) + j = max(newCam,baseCam) + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + vec[idx] = randrange(threshold,maxval+1) + + # now run thru the daisychains! + for dc in daisyChainers: + while len(dc) > 0: + chainer = dc.pop() + if len(dc)==0: # inefficient... + i = min(baseCam,chainer) # the last guy in the chain really ought to connect to the base... + j = max(baseCam,chainer) + else: + i = min(dc[-1],chainer) + j = max(dc[-1],chainer) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) # https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.squareform.html + vec[idx] = randrange(ceil( threshold*sqrt(len(allDaisyChainers)+nChains) ),ceil( maxval*sqrt(len(allDaisyChainers)+nChains) )+1) # make sure the entries among the daisy chainers actually connect... + + with open(csvfile,"a") as f: + wrobj = csv.writer(f,delimiter='\t') + row = ["nCams",nCams,"baseCam",baseCam] + for chain in temp: + row += ["longDaisyChainers"]+chain + + row += ["pairCounts"]+vec + wrobj.writerow(row) + + # nothing here guarantees uniqueness or non-overlappingness of the long chains. this shouldn't be a problem. + + +# %% +# constructor 5: the existence of one fella on an island +csvfile = os.path.join('..','csv','testCase5.csv') + +# cleanup +with open(csvfile,"w+") as _: + pass + +for _ in range(Niter): + nCams = randrange(minCams,maxCams+1) + baseCam = randrange(0,nCams) + + # we frankly don't care about thresholds here, we'll just make a bunch of values and set the important ones to be above threshold later + veclen = nCams*(nCams-1) // 2 + vec = [randrange(maxval+1) for _ in range(veclen)] + + islander = randrange(nCams-1) + islander = islander+1 if islander>=baseCam else islander + + # now for each guy connected to the basecam: + for newCam in range(nCams): + if newCam == baseCam: + pass + else: + i = min(newCam,baseCam) + j = max(newCam,baseCam) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + if newCam != islander: + vec[idx] = randrange(threshold,maxval) + + # now cut off the islander + i = min(newCam,islander) + j = max(newCam,islander) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + vec[idx] = randrange(0,ceil(threshold*sqrt(2))-threshold) # make sure it's an ESPECIALLY bad connection, otherwise the tendency will frequently (and correctly) be to try and daisychain the islander, which makes validation difficult! (but which is a behavior that probably needs to be validated per se, huh? ughhh but the way I implemented it is proving to make a quick & dirty unit test much less quick and much more dirty than I anticipated... welcome to edge case town) + else: + # make sure the base cam is the best candidate for the islander to connect to (otherwise the unit test programming gets pretty tricky...) + vec[idx] = randrange( ceil(threshold*sqrt(2))-threshold ,threshold); + + with open(csvfile,"a") as f: + wrobj = csv.writer(f,delimiter='\t') + vec = ["nCams",nCams,"baseCam",baseCam,"islander",islander,"pairCounts"] + vec + wrobj.writerow(vec) + + +# %% +# constructor 6: several fellas lack a valid path +csvfile = os.path.join('..','csv','testCase6.csv') + +# cleanup +with open(csvfile,"w+") as _: + pass + +for _ in range(Niter): + nCams = randrange(minCams,maxCams+1) + baseCam = randrange(0,nCams) + + # we frankly don't care about thresholds here, we'll just make a bunch of values and set the important ones to be above (AND BELOW) threshold later + veclen = nCams*(nCams-1) // 2 + vec = [randrange(maxval+1) for _ in range(veclen)] + + # now for each guy connected to the basecam: + nIslanders = randrange(2,nCams//2+1) + islanders = [i for i in range(nCams-1)] + shuffle(islanders) + islanders = islanders[:nIslanders] + islanders = [islander+1 if islander>=baseCam else islander for islander in islanders] + + for newCam in range(nCams): + if newCam == baseCam: + pass + else: + i = min(newCam,baseCam) + j = max(newCam,baseCam) + + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + # if newCam is not an islander, just connect it to the base and be done + if newCam not in islanders: + vec[idx] = randrange(threshold,maxval) + else: + # if newCam *is* an islander, you've got your work cut out for you. + # first, disconnect it from the base (but not TOO hard) + vec[idx] = randrange(ceil(threshold*sqrt(2))-threshold,threshold) + + # next, disconnect it HARD from all NON-BASE nodes (inefficiently I might add, but this is a one-time script, not a bottleneck) + for newerCam in range(nCams): + if newerCam == newCam or newerCam == baseCam: + pass + else: + i = min(newCam,newerCam) + j = max(newCam,newerCam) + idx = comb(nCams,2,exact=True) - comb(nCams-i,2,exact=True) + (j-i-1) + + vec[idx] = randrange(0,ceil(threshold*sqrt(2))-threshold) + + + + + with open(csvfile,"a") as f: + wrobj = csv.writer(f,delimiter='\t') + vec = ["nCams",nCams,"baseCam",baseCam,"islanders"] + islanders + ["pairCounts"] + vec + wrobj.writerow(vec) \ No newline at end of file