[NOTE] This question can be depreciated in favor of version 0.31.
This is a code revision of a previous post and works well.
The purpose of this code is to produce a universe of points, randomly generated around predetermined centroids, provided from the user as either a vector of vectors or from a file. The final product is a file of sample points produced around the centroids, to be used for fake data analysis in another program. The objective here is brevity and speed. This most recent version is still under 200 lines, which I find acceptable but would prefer not to get much longer unless there is significant utility-gain reasoning.
Changes in This Version:
- Placed
#include
s in more appropriate locations (within the appropriate.h
or.cpp
and within the include guard) - Modified strongly away from procedural to OO. While this nearly doubled the code (from ~100 to ~200 lines), this simplified user parameter input. Overall, I feel the utility gain was worth it for the line-number increase.
- Point generation has been forced into a Normal distribution (for now - please see "Future Implementations" below)
- I went through a major debate whether to instance all points in a vector then output to file, or simply output to file on the fly. Eventually the later option won out for two reasons: it severely reduces memory requirements for mega-sets and it makes for less code in the overall program.
- The move to OO changed nearly every aspect of the code; this is very different than the previous versions, but maintains similar output.
My goal is two-fold: speed and conciseness. Speed takes precedence over conciseness, but thankfully they tend to go hand in hand.
Future Implementations:
- Concisification is an ongoing process.
- Reporting is intrusive throughout the program - I'm almost considering getting rid of the report file entirely to reduce code. Creating a new function for reporting will not reduce code as far as I can tell. Would love some thoughts on this...
- User-supplied distribution function (it is currently forced to Normal). This is a priority for me - I attempted to instance a function pointer in the
cluster_set
object along with a default function, which could then be tweaked by the user to whatever statistical distribution they desire. This failed miserably, so I've forced the distribution to Normality for the time being.
Shout out to Edward and Justin - your input on v0.2 was invaluable!
clustergen.h
#ifndef CLUSTERGEN_H
#define CLUSTERGEN_H
#include <fstream>
#include <vector>
#include <string>
class cluster_set {
std::vector<std::vector<double>> centroids; // Centroids around which to evenly generate all points
std::string file_out_str; // The output file name - only settable on construction
std::string file_rpt_str; // The report file name - only settable on construction
std::ofstream file_out; // Instanced on clustergen() to prevent multiple outputs to the same file
std::ofstream file_rpt; // Appends throughout program
char delim_in; // Defaults to CSV - only used in file import
char delim_out; // Defaults to CSV
public:
cluster_set(); // Default settings
cluster_set(const std::string &, const std::string &); // User specified output and report file
void import_centroids(const std::string &); // File import - uses vector import
void import_centroids(const std::vector<std::vector<double>> &); // Vector import - primary import algo
void clustergen(const unsigned int); // Primary algorithm - generates [unsigned int] number of points
void set_delim_in(const char d) { this->delim_in = d; }; // User specified delimiter for input
void set_delim_out(const char d) { this->delim_out = d; }; // User specified delimiter for output
};
#endif //CLUSTERGEN_H
clustergen.cpp
#include "clustergen.h"
#include <sstream>
#include <iostream>
#include <chrono>
#include <random>
// Default everything
cluster_set::cluster_set(void) {
this->file_out_str = "clustergen_out.dat";
this->file_rpt_str = "clustergen_rpt.dat";
this->delim_in = ',';
this->delim_out = ',';
this->file_rpt.open(this->file_rpt_str);
this->file_rpt << "CLUSTERGEN STATUS REPORT FOLLOWS";
this->file_rpt.close();
}
// User specified output and report file
cluster_set::cluster_set(const std::string &fout, const std::string &rout) :
file_out_str(fout),
file_rpt_str(rout) {
this->delim_in = ',';
this->delim_out = ',';
this->file_rpt.open(this->file_rpt_str);
this->file_rpt << "CLUSTERGEN STATUS REPORT FOLLOWS";
this->file_rpt.close();
}
// Reads in a file to a vector then passes to the primary centroid import function
void cluster_set::import_centroids(const std::string &fin) {
std::ifstream file_in(fin);
this->file_rpt.open(this->file_rpt_str, std::ios_base::app);
this->file_rpt << "\n" << "\n" << "Importing file to centroid vector: \"" << fin << "\" ...";
std::string line;
std::vector<std::vector<double>> temp_centroid_vec;
while (std::getline(file_in, line)) {
while ((line.length() == 0) && !(file_in.eof())) {
std::getline(file_in, line);
} // Skips blank lines in file
std::string param;
std::stringstream ss(line);
std::vector<double> temp_point;
if ((line.length() != 0)) {
while (std::getline(ss, param, this->delim_in)) {
temp_point.push_back(atof(param.c_str()));
}
temp_centroid_vec.push_back(temp_point);
}
}
this->file_rpt.close();
this->import_centroids(temp_centroid_vec);
}
// Primary centroid import function
void cluster_set::import_centroids(const std::vector<std::vector<double>> ¢roid_vec) {
this->file_rpt.open(this->file_rpt_str, std::ios_base::app);
this->file_rpt << "\n" << "\n" << "Importing centroid vector to universe:";
if (!(this->centroids.empty())) {
this->file_rpt << "\n" << " WARNING: Universe already initialized - flushing all clusters ...";
this->centroids.clear();
}
for (auto cent_vec_iter = centroid_vec.begin(); cent_vec_iter != centroid_vec.end(); ++cent_vec_iter) {
if (this->centroids.empty()) {
this->centroids.push_back(*cent_vec_iter);
} else if (cent_vec_iter->size() == this->centroids.front().size()) {
this->centroids.push_back(*cent_vec_iter);
}
}
this->file_rpt << "\n" << " " << this->centroids.size() << " Centroid imports SUCCESSFUL.";
this->file_rpt << "\n" << " " << centroid_vec.size() - this->centroids.size() << " Centroid imports FAILED.";
this->file_rpt.close();
}
// Primary cluster generator - aborts if no centroids exists.
void cluster_set::clustergen(const unsigned int k) {
static std::default_random_engine gen(
std::chrono::system_clock::now().time_since_epoch().count()); // Random seed
this->file_rpt.open(this->file_rpt_str, std::ios_base::app);
this->file_out.open(this->file_out_str);
this->file_rpt << "\n" << "\n" << "Generating Points:";
if (this->centroids.empty()) {
this->file_rpt << "\n" << "ERROR: No centroids have been imported. Aborting operation.";
return;
}
const unsigned int n = k / this->centroids.size();
unsigned rem = k % this->centroids.size();
unsigned int centroid_ct = 0;
for (auto centroid_iter = this->centroids.begin(); centroid_iter != this->centroids.end(); ++centroid_iter) {
unsigned int subset = n + (rem ? 1 : 0);
this->file_rpt << "\n" << " Generating " << subset << " points around centroid " << ++centroid_ct
<< " ...";
while (subset) {
std::vector<double> temp_point;
for (auto dimension_iter = centroid_iter->begin();
dimension_iter != centroid_iter->end(); ++dimension_iter) {
// Would love to put something here to allow users to specify their own distribution...
std::normal_distribution<double> distr(*dimension_iter, 10);
temp_point.push_back(distr(gen));
}
for (auto temp_point_iter = temp_point.begin();
temp_point_iter != temp_point.end(); ++temp_point_iter) {
if (temp_point_iter != temp_point.begin()) { this->file_out << this->delim_out; }
this->file_out << (*temp_point_iter);
}
if (subset - 1) { this->file_out << "\n"; };
--subset;
if (rem) { --rem; }
}
this->file_rpt << " Done.";
auto centroid_iter_peek = centroid_iter;
++centroid_iter_peek;
if (centroid_iter_peek != centroids.end()) { this->file_out << "\n"; };
}
this->file_rpt << "\n\n " << k << " total points generated around " << centroid_ct << " centroids.";
this->file_out.close();
this->file_rpt.close();
}
main.h
(Contains a few examples)
#include "clustergen.h"
int main() {
std::vector<std::vector<double>> v = {{11, 22},
{33, 44},
{55, 66, 77},
{88, 99},
{100}};
std::vector<std::vector<double>> v2 = {{110, 120},
{130, 140},
{150, 160, 170},
{180, 190},
{200}};
cluster_set my_clusters;
// Vector import with default out file, reporting file, and delimiters.
my_clusters.import_centroids(v);
my_clusters.clustergen(10);
cluster_set my_clusters1("1_clustergen_out.dat", "1_clustergen_rpt.dat");
// Vector import with user specified out file and reporting file, and default delimiters.
my_clusters1.import_centroids(v2);
my_clusters1.clustergen(11);
cluster_set my_clusters2("2_clustergen_out.dat", "2_clustergen_rpt.dat");
// Vector import with user specified out file, reporting file, and delimiters.
my_clusters2.set_delim_in(' ');
my_clusters2.set_delim_out('@');
my_clusters2.import_centroids(v);
my_clusters2.clustergen(12);
cluster_set my_clusters3("3_clustergen_out.dat", "3_clustergen_rpt.dat");
// File import with default out file, reporting file, and delimiters.
my_clusters3.set_delim_in(' ');
my_clusters3.import_centroids("clustergen_in.dat");
my_clusters3.clustergen(13);
cluster_set my_clusters4("4_clustergen_out.dat", "4_clustergen_rpt.dat");
// File import with user specified out file and reporting file, and default delimiters.
my_clusters4.set_delim_in('#');
my_clusters4.import_centroids("clustergen_in2.dat");
my_clusters4.clustergen(14);
cluster_set my_clusters5("5_clustergen_out.dat", "5_clustergen_rpt.dat");
// File import with user specified out file, reporting file, and delimiters.
my_clusters5.set_delim_in('#');
my_clusters5.set_delim_out('@');
my_clusters5.import_centroids("clustergen_in2.dat");
my_clusters5.clustergen(15);
}
clustergen_in.dat
(Used in the main.h
examples above)
210 220
230 240
250 260 270
280 290
200
clustergen_in2.dat
(Used in the main.h
examples above)
210#220
230#240
250#260#270
280#290
200