<commit_before>/* -*- mode: espresso; espresso-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set softtabstop=2 shiftwidth=2 tabstop=2 expandtab: */

"use strict";

var CircuitGraphPlot = function() {
  this.widgetID = this.registerInstance();
  this.registerSource();

	// Each entry has an array of one or more SkeletonModel instances
	this.models = [];

  // Node ids, each has one or more models in this.models
  // and the order corresponds with that of the adjacency matrix.
  this.ids = [];

  // Name of each node
  this.names = [];

  this.AdjM = null;

	// From CircuitGraphAnalysis, first array entry is Signal Flow, rest are
	// the sorted pairs of [eigenvalue, eigenvector].
	this.vectors = null;

  // Array of arrays, containing anatomical measurements
  this.anatomy = null;

  // Array of arrays, containing centrality measures
  this.centralities = [null];

  this.names_visible = true;

  // Node ID vs true
  this.selected = {};

  // Parameters for anatomy
  this.reroot_at_soma = true;
  this.sigma = 200; // nm
  this.bandwidth = 8000; // nm
  this.prune_bare_terminal_segments = false;

  this.pca_graph = {
    'Graph': false,
    'Betweenness centrality': false
  };

  this.pca_anatomy_absolute = {
    'Cable length': true,
    'Cable minus principal branch': false,
    'Histogram of asymmetry index': false,
    'Histogram of cable asymmetry index': false,
    'Cable of terminal segments': false,
    'Num. terminal segments': false,
    'Num. of branches': false
  };

  this.pca_anatomy_relative = {
    'Asymmetry index': false,
    'Normalized histogram of asymmetry index': false,
    'Cable asymmetry index': false,
    'Normalized histogram of cable asymmetry index': false,
    'Normalized cable of terminal segments': false
  };

  this.pca_synapses_absolute = {
    'Num. of inputs': false,
    'Num. of outputs': false,
    'Histogram of output asymmetry index': false,
    'Histogram of input asymmetry index': false,
    'Cable of hillock': true
  };

  this.pca_synapses_relative = {
    'Segregation index': true,
    'Ratio (I - O) / (I + O)': false,
    'Output asymmetry index': true,
    'Normalized histogram of output asymmetry index': true,
    'Input asymmetry index': true,
    'Normalized histogram of input asymmetry index': true,
  };


  // Array of pairs of [single value, principal component vector]
  this.pca = null;
};

CircuitGraphPlot.prototype = {};
$.extend(CircuitGraphPlot.prototype, new InstanceRegistry());
$.extend(CircuitGraphPlot.prototype, new SkeletonSource());

CircuitGraphPlot.prototype.getName = function() {
	return "Circuit Graph Plot " + this.widgetID;
};

CircuitGraphPlot.prototype.destroy = function() {
  this.unregisterInstance();
  this.unregisterSource();
  neuronNameService.unregister(this);
  
  Object.keys(this).forEach(function(key) { delete this[key]; }, this);
};

CircuitGraphPlot.prototype.updateModels = function(models) {
	this.append(models);
};

/** Returns a clone of all skeleton models, keyed by skeleton ID. */
CircuitGraphPlot.prototype.getSelectedSkeletonModels = function() {
  if (!this.svg) return {};
  var models = this.models,
      selected = this.selected;
	return this.ids.reduce(function(o, id, i) {
    if (selected[id]) {
      return models[i].reduce(function(o, model) {
        o[model.id] = model.clone();
        return o;
      }, o);
    }
    return o;
  }, {});
};

CircuitGraphPlot.prototype.getSelectedSkeletons = function() {
  if (!this.svg) return [];
  var models = this.models,
      selected = this.selected;
	return this.ids.reduce(function(a, id, i) {
    if (selected[id]) {
      return a.concat(models[i].map(function(model) { return model.id; }));
    }
    return a;
  }, []);
};

CircuitGraphPlot.prototype.getSkeletons = function() {
  if (!this.models) return [];
  var models = this.models;
	return this.ids.reduce(function(a, id, i) {
    return a.concat(models[i].map(function(model) { return model.id; }));
  }, []);
};

CircuitGraphPlot.prototype.getSkeletonModels = function() {
  if (!this.models) return {};
  return this.models.reduce(function(o, ms) {
    return ms.reduce(function(o, model) {
      o[model.id] = model;
      return o;
    }, o);
  }, {});
};

CircuitGraphPlot.prototype.getSkeletonModel = function(skeleton_id) {
  for (var i=0; i<this.models.length; ++i) {
    if (skeleton_id === this.models[i].id) return this.models[i].clone();
  }
  return null;
};

CircuitGraphPlot.prototype.hasSkeleton = function(skeleton_id) {
  return this.models.some(function(ms) {
    return ms.some(function(model) {
      return skeleton_id === model.id;
    });
  });
};

CircuitGraphPlot.prototype.clear = function() {
	this.models = [];
  this.ids = [];
  this.names = [];
  this.selected = {};
  this.clearGUI();
  this.vectors = null;
  this.anatomy = null;
  this.centralities = [null];
  this.pca = null;
};

CircuitGraphPlot.prototype.append = function(models) {
	// Update names and colors when already present, or remove when deselected
  var skeleton_ids = {};
	this.models = this.models.filter(function(ms, i) {
    var ms2 = ms.filter(function(m, k) {
      skeleton_ids[m.id] = true;
      //
      var model = models[m.id];
      if (!model) return true;
      if (model.selected) {
        ms[k] = model.clone(); // replace first
        if (1 === ms.length) this.names[i] = model.baseName;
        return true;
      }
      return false; // remove skeleton from group
    }, this);
    if (0 === ms2.length) {
      // Remove group
      this.ids.splice(k, 1);
      this.names.splice(k, 1);
      return false; // removes from this.models
    }
    return true;
  }, this);

  // Add new ones
  Object.keys(models).forEach(function(skid) {
    if (skid in skeleton_ids) return;
    var model = models[skid];
    if (model.selected) {
      skeleton_ids[model.id] = true;
      //
      this.ids.push(model.id);
      this.names.push(model.baseName);
      this.models.push([model]);
    }
  }, this);

  var skids = Object.keys(skeleton_ids);

  if (skids.length < 2) {
    this.clearGUI();
    growlAlert('Need more than one', 'Need at least two neurons!');
    return;
  }

	// fetch connectivity data, create adjacency matrix and plot it
  // register with name service before we go about the plot
  requestQueue.register(django_url + project.id + '/skeletongroup/skeletonlist_confidence_compartment_subgraph', 'POST',
			{skeleton_list: skids},
			(function(status, text) {
				if (200 !== status) return;
				var json = $.parseJSON(text);
				if (json.error) { alert(json.error); return; }
				// Create adjacency matrix
				var AdjM = this.ids.map(function(id) { return new Uint32Array(this.ids.length); }, this);
        // Create indices from skeleton ID to group index in this.ids array
				var indices = this.models.reduce(function(o, ms, i) {
          return ms.reduce(function(o, model) {
            o[model.id] = i;
            return o;
          }, o);
        }, {});
				// Populate adjacency matrix, accumulating edge synapse counts for groups
				json.edges.forEach(function(edge) {
					AdjM[indices[edge[0]]][indices[edge[1]]] += edge[2];
				});
        // Update data and GUI
        this.plot(this.ids, this.names, this.models, AdjM);
		}).bind(this));
};

/**
 * This method is called from the neuron name service, if neuron names are
 * changed.
 */
CircuitGraphPlot.prototype.updateNeuronNames = function() {
  this.redraw();
};


CircuitGraphPlot.prototype._add_graph_partition = function(mirror) {
  // Potentially disjoint if there are least two network components,
  // detectable by finding out whether the first non-zero eigenvalue has zeros where the next one doesn't.
  var epsilon = 0.00000001,
      clean = function(v) { return Math.abs(v) < epsilon ? 0 : v},
      ev2 = this.vectors[1][1].map(clean),
      ev3 = this.vectors[2][1].map(clean);

  if (mirror) {
    ev3 = ev3.map(function(v) { return -v; });
  }

  if (ev2.some(function(v2, i) {
    var v3 = ev3[i];
    return (0 === v2 && 0 !== v3) || (0 === v3 && 0 !== v2);
  })) {
    this.vectors.push([-1, ev2.map(function(v2, i) {
      return 0 === v2 ? ev3[i] : v2;
    })]);
  } else if (this.vectors.length > 3) {
    // Not disjoint: combine the third and fourth eigenvectors
    // as a function of the second eigenvector, according to the sign in the latter.
    
    var ev4 = this.vectors[3][1].map(clean),
        vs = [ev3, ev4];

    // Pick all indices for positive values in the second (1) eigenvector
    var positive = ev2.reduce(function(a, v, i) { if (v > 0) a.push(i); return a; }, []);

    // For the positive indices, find out if the std dev is larger in the third
    // or the fourth eigenvectors
    var indices = [0, 1].map(function(k) {
      var v = vs[k],
          mean = positive.reduce(function(sum, i) { return sum + v[i];}, 0) / positive.length,
          stdDev = positive.reduce(function(sum, i) { return sum + Math.pow(v[i] - mean, 2); }, 0) / positive.length;
      return [k, stdDev];
    }, this).sort(function(a, b) {
      return a[1] < b[1];
    }).map(function(a) { return a[0]; });

    // Create a new vector with the most signal from both the third (2) and fourth (3) eigenvectors
    this.vectors.push([-1, ev2.map(function(v, i) {
      return vs[v > 0 ? indices[0] : indices[1]][i]; 
    }, this)]);
  } else {
    this.vectors.push([-1, this.ids.map(function() { return 0; })]);
  }
};

/** Takes an array of skeleton IDs, a map of skeleton ID vs SkeletonModel,
 * and an array of arrays representing the adjacency matrix where the order
 * in rows and columns corresponds to the order in the array of skeleton IDs.
 * Clears the existing plot and replaces it with the new data. */
CircuitGraphPlot.prototype.plot = function(ids, names, models, AdjM) {
  // Set the new data
  this.ids = ids;
  this.names = names;
  this.models = models;
  this.AdjM = AdjM;
  this.selected = {};

  neuronNameService.registerAll(this,
      this.getSkeletonModels(),
      (function() { this._plot(); }).bind(this));
};

CircuitGraphPlot.prototype._plot = function() {
  // Compute signal flow and eigenvectors
  try {
    var cga = new CircuitGraphAnalysis().init(this.AdjM, 100000, 0.0000000001);
  } catch (e) {
    this.clear();
    console.log(e, e.stack);
    alert("Failed to compute the adjacency matrix: \n" + e + "\n" + e.stack);
    return;
  }

  // Reset data
  this.vectors = [];
  this.anatomy = null;
  this.centralities = [null];
  this.pca = null;

  // Can be null when not computable
  if (cga.e && cga.z) {
    // Store for replotting later
    this.vectors = [[-1, cga.z]];
    for (var i=0; i<10 && i <cga.e.length; ++i) {
      this.vectors.push(cga.e[i]);
    }

    if (cga.e.length > 2) {
      this._add_graph_partition(false);
      this._add_graph_partition(true);
    }
  }

  this.updatePulldownMenus(false);

  this.redraw();
};

CircuitGraphPlot.prototype.updatePulldownMenus = function(preserve_indices) {
  // Reset pulldown menus
  var updateSelect = (function(select) {
    var index = select.selectedIndex;
    select.options.length = 0;

    if (this.vectors.length > 0) {
      // Will be zero when neurons don't connect to each other<commit_msg>CircuitGraphPlot: fix error when no neurons are connected.<commit_after>/* -*- mode: espresso; espresso-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set softtabstop=2 shiftwidth=2 tabstop=2 expandtab: */

"use strict";

var CircuitGraphPlot = function() {
  this.widgetID = this.registerInstance();
  this.registerSource();

	// Each entry has an array of one or more SkeletonModel instances
	this.models = [];

  // Node ids, each has one or more models in this.models
  // and the order corresponds with that of the adjacency matrix.
  this.ids = [];

  // Name of each node
  this.names = [];

  this.AdjM = null;

	// From CircuitGraphAnalysis, first array entry is Signal Flow, rest are
	// the sorted pairs of [eigenvalue, eigenvector].
	this.vectors = null;

  // Array of arrays, containing anatomical measurements
  this.anatomy = null;

  // Array of arrays, containing centrality measures
  this.centralities = [null];

  this.names_visible = true;

  // Node ID vs true
  this.selected = {};

  // Parameters for anatomy
  this.reroot_at_soma = true;
  this.sigma = 200; // nm
  this.bandwidth = 8000; // nm
  this.prune_bare_terminal_segments = false;

  this.pca_graph = {
    'Graph': false,
    'Betweenness centrality': false
  };

  this.pca_anatomy_absolute = {
    'Cable length': true,
    'Cable minus principal branch': false,
    'Histogram of asymmetry index': false,
    'Histogram of cable asymmetry index': false,
    'Cable of terminal segments': false,
    'Num. terminal segments': false,
    'Num. of branches': false
  };

  this.pca_anatomy_relative = {
    'Asymmetry index': false,
    'Normalized histogram of asymmetry index': false,
    'Cable asymmetry index': false,
    'Normalized histogram of cable asymmetry index': false,
    'Normalized cable of terminal segments': false
  };

  this.pca_synapses_absolute = {
    'Num. of inputs': false,
    'Num. of outputs': false,
    'Histogram of output asymmetry index': false,
    'Histogram of input asymmetry index': false,
    'Cable of hillock': true
  };

  this.pca_synapses_relative = {
    'Segregation index': true,
    'Ratio (I - O) / (I + O)': false,
    'Output asymmetry index': true,
    'Normalized histogram of output asymmetry index': true,
    'Input asymmetry index': true,
    'Normalized histogram of input asymmetry index': true,
  };


  // Array of pairs of [single value, principal component vector]
  this.pca = null;
};

CircuitGraphPlot.prototype = {};
$.extend(CircuitGraphPlot.prototype, new InstanceRegistry());
$.extend(CircuitGraphPlot.prototype, new SkeletonSource());

CircuitGraphPlot.prototype.getName = function() {
	return "Circuit Graph Plot " + this.widgetID;
};

CircuitGraphPlot.prototype.destroy = function() {
  this.unregisterInstance();
  this.unregisterSource();
  neuronNameService.unregister(this);
  
  Object.keys(this).forEach(function(key) { delete this[key]; }, this);
};

CircuitGraphPlot.prototype.updateModels = function(models) {
	this.append(models);
};

/** Returns a clone of all skeleton models, keyed by skeleton ID. */
CircuitGraphPlot.prototype.getSelectedSkeletonModels = function() {
  if (!this.svg) return {};
  var models = this.models,
      selected = this.selected;
	return this.ids.reduce(function(o, id, i) {
    if (selected[id]) {
      return models[i].reduce(function(o, model) {
        o[model.id] = model.clone();
        return o;
      }, o);
    }
    return o;
  }, {});
};

CircuitGraphPlot.prototype.getSelectedSkeletons = function() {
  if (!this.svg) return [];
  var models = this.models,
      selected = this.selected;
	return this.ids.reduce(function(a, id, i) {
    if (selected[id]) {
      return a.concat(models[i].map(function(model) { return model.id; }));
    }
    return a;
  }, []);
};

CircuitGraphPlot.prototype.getSkeletons = function() {
  if (!this.models) return [];
  var models = this.models;
	return this.ids.reduce(function(a, id, i) {
    return a.concat(models[i].map(function(model) { return model.id; }));
  }, []);
};

CircuitGraphPlot.prototype.getSkeletonModels = function() {
  if (!this.models) return {};
  return this.models.reduce(function(o, ms) {
    return ms.reduce(function(o, model) {
      o[model.id] = model;
      return o;
    }, o);
  }, {});
};

CircuitGraphPlot.prototype.getSkeletonModel = function(skeleton_id) {
  for (var i=0; i<this.models.length; ++i) {
    if (skeleton_id === this.models[i].id) return this.models[i].clone();
  }
  return null;
};

CircuitGraphPlot.prototype.hasSkeleton = function(skeleton_id) {
  return this.models.some(function(ms) {
    return ms.some(function(model) {
      return skeleton_id === model.id;
    });
  });
};

CircuitGraphPlot.prototype.clear = function() {
	this.models = [];
  this.ids = [];
  this.names = [];
  this.selected = {};
  this.clearGUI();
  this.vectors = null;
  this.anatomy = null;
  this.centralities = [null];
  this.pca = null;
};

CircuitGraphPlot.prototype.append = function(models) {
	// Update names and colors when already present, or remove when deselected
  var skeleton_ids = {};
	this.models = this.models.filter(function(ms, i) {
    var ms2 = ms.filter(function(m, k) {
      skeleton_ids[m.id] = true;
      //
      var model = models[m.id];
      if (!model) return true;
      if (model.selected) {
        ms[k] = model.clone(); // replace first
        if (1 === ms.length) this.names[i] = model.baseName;
        return true;
      }
      return false; // remove skeleton from group
    }, this);
    if (0 === ms2.length) {
      // Remove group
      this.ids.splice(k, 1);
      this.names.splice(k, 1);
      return false; // removes from this.models
    }
    return true;
  }, this);

  // Add new ones
  Object.keys(models).forEach(function(skid) {
    if (skid in skeleton_ids) return;
    var model = models[skid];
    if (model.selected) {
      skeleton_ids[model.id] = true;
      //
      this.ids.push(model.id);
      this.names.push(model.baseName);
      this.models.push([model]);
    }
  }, this);

  var skids = Object.keys(skeleton_ids);

  if (skids.length < 2) {
    this.clearGUI();
    growlAlert('Need more than one', 'Need at least two neurons!');
    return;
  }

	// fetch connectivity data, create adjacency matrix and plot it
  // register with name service before we go about the plot
  requestQueue.register(django_url + project.id + '/skeletongroup/skeletonlist_confidence_compartment_subgraph', 'POST',
			{skeleton_list: skids},
			(function(status, text) {
				if (200 !== status) return;
				var json = $.parseJSON(text);
				if (json.error) { alert(json.error); return; }
				// Create adjacency matrix
				var AdjM = this.ids.map(function(id) { return new Uint32Array(this.ids.length); }, this);
        // Create indices from skeleton ID to group index in this.ids array
				var indices = this.models.reduce(function(o, ms, i) {
          return ms.reduce(function(o, model) {
            o[model.id] = i;
            return o;
          }, o);
        }, {});
				// Populate adjacency matrix, accumulating edge synapse counts for groups
				json.edges.forEach(function(edge) {
					AdjM[indices[edge[0]]][indices[edge[1]]] += edge[2];
				});
        // Update data and GUI
        this.plot(this.ids, this.names, this.models, AdjM);
		}).bind(this));
};

/**
 * This method is called from the neuron name service, if neuron names are
 * changed.
 */
CircuitGraphPlot.prototype.updateNeuronNames = function() {
  this.redraw();
};


CircuitGraphPlot.prototype._add_graph_partition = function(mirror) {
  // Potentially disjoint if there are least two network components,
  // detectable by finding out whether the first non-zero eigenvalue has zeros where the next one doesn't.
  var epsilon = 0.00000001,
      clean = function(v) { return Math.abs(v) < epsilon ? 0 : v},
      ev2 = this.vectors[1][1].map(clean),
      ev3 = this.vectors[2][1].map(clean);

  if (mirror) {
    ev3 = ev3.map(function(v) { return -v; });
  }

  if (ev2.some(function(v2, i) {
    var v3 = ev3[i];
    return (0 === v2 && 0 !== v3) || (0 === v3 && 0 !== v2);
  })) {
    this.vectors.push([-1, ev2.map(function(v2, i) {
      return 0 === v2 ? ev3[i] : v2;
    })]);
  } else if (this.vectors.length > 3) {
    // Not disjoint: combine the third and fourth eigenvectors
    // as a function of the second eigenvector, according to the sign in the latter.
    
    var ev4 = this.vectors[3][1].map(clean),
        vs = [ev3, ev4];

    // Pick all indices for positive values in the second (1) eigenvector
    var positive = ev2.reduce(function(a, v, i) { if (v > 0) a.push(i); return a; }, []);

    // For the positive indices, find out if the std dev is larger in the third
    // or the fourth eigenvectors
    var indices = [0, 1].map(function(k) {
      var v = vs[k],
          mean = positive.reduce(function(sum, i) { return sum + v[i];}, 0) / positive.length,
          stdDev = positive.reduce(function(sum, i) { return sum + Math.pow(v[i] - mean, 2); }, 0) / positive.length;
      return [k, stdDev];
    }, this).sort(function(a, b) {
      return a[1] < b[1];
    }).map(function(a) { return a[0]; });

    // Create a new vector with the most signal from both the third (2) and fourth (3) eigenvectors
    this.vectors.push([-1, ev2.map(function(v, i) {
      return vs[v > 0 ? indices[0] : indices[1]][i]; 
    }, this)]);
  } else {
    this.vectors.push([-1, this.ids.map(function() { return 0; })]);
  }
};

/** Takes an array of skeleton IDs, a map of skeleton ID vs SkeletonModel,
 * and an array of arrays representing the adjacency matrix where the order
 * in rows and columns corresponds to the order in the array of skeleton IDs.
 * Clears the existing plot and replaces it with the new data. */
CircuitGraphPlot.prototype.plot = function(ids, names, models, AdjM) {
  // Set the new data
  this.ids = ids;
  this.names = names;
  this.models = models;
  this.AdjM = AdjM;
  this.selected = {};

  neuronNameService.registerAll(this,
      this.getSkeletonModels(),
      (function() { this._plot(); }).bind(this));
};

CircuitGraphPlot.prototype._plot = function() {
  // Compute signal flow and eigenvectors
  try {
    var cga = new CircuitGraphAnalysis().init(this.AdjM, 100000, 0.0000000001);
  } catch (e) {
    this.clear();
    console.log(e, e.stack);
    alert("Failed to compute the adjacency matrix: \n" + e + "\n" + e.stack);
    return;
  }

  // Reset data
  this.vectors = [];
  this.anatomy = null;
  this.centralities = [null];
  this.pca = null;

  // Can be null when not computable
  if (cga && cga.e && cga.z) {
    // Store for replotting later
    this.vectors = [[-1, cga.z]];
    for (var i=0; i<10 && i <cga.e.length; ++i) {
      this.vectors.push(cga.e[i]);
    }

    if (cga.e.length > 2) {
      this._add_graph_partition(false);
      this._add_graph_partition(true);
    }
  }

  this.updatePulldownMenus(false);

  this.redraw();
};

CircuitGraphPlot.prototype.updatePulldownMenus = function(preserve_indices) {
  // Reset pulldown menus
  var updateSelect = (function(select) {
    var index = select.selectedIndex;
    select.options.length = 0;

    if (this.vectors.length > 0) {
      // Will be zero when neurons don't connect to each other
{'fraction_non_alphanumeric': 0.14328942684229262, 'fraction_numerical': 0.010145959416162335, 'mean_word_length': 3.0572305470301497, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 4, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2556931', 'n_tokens_mistral': 7764, 'n_tokens_neox': 7220, 'n_words': 2003}