content: /*************************************************************************
  *                                                                        *
  *  This file is part of the 20n/act project.                             *
  *  20n/act enables DNA prediction for synthetic biology/bioengineering.  *
  *  Copyright (C) 2017 20n Labs, Inc.                                     *
  *                                                                        *
  *  Please direct all queries to act@20n.com.                             *
  *                                                                        *
  *  This program is free software: you can redistribute it and/or modify  *
  *  it under the terms of the GNU General Public License as published by  *
  *  the Free Software Foundation, either version 3 of the License, or     *
  *  (at your option) any later version.                                   *
  *                                                                        *
  *  This program is distributed in the hope that it will be useful,       *
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of        *
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
  *  GNU General Public License for more details.                          *
  *                                                                        *
  *  You should have received a copy of the GNU General Public License     *
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>. *
  *                                                                        *
  *************************************************************************/
  
  package com.act.analysis.surfactant;
  
  import chemaxon.marvin.calculations.logPPlugin;
  import org.apache.commons.lang3.tuple.Pair;
  
  import java.util.Collection;
  import java.util.HashSet;
  import java.util.Map;
  import java.util.Set;
  
  public class AtomSplit {
    Set<Integer> leftIndices;
    Set<Integer> rightIndices;
  
    double leftSum = 0.0, rightSum = 0.0;
    double weightedLeftSum = 0.0, weightedRightSum = 0.0;
    int leftPosCount = 0, leftNegCount = 0;
    int rightPosCount = 0, rightNegCount = 0;
    double leftMin = 0.0, leftMax = 0.0;
    double rightMin = 0.0, rightMax = 0.0;
  
    protected AtomSplit() { }
  
    public AtomSplit(AtomSplit toCopy) {
      this.leftIndices = new HashSet<>(toCopy.leftIndices);
      this.rightIndices = new HashSet<>(toCopy.rightIndices);
      this.leftSum = toCopy.leftSum;
      this.rightSum = toCopy.rightSum;
      this.weightedLeftSum = toCopy.weightedLeftSum;
      this.weightedRightSum = toCopy.weightedRightSum;
      this.leftPosCount = toCopy.leftPosCount;
      this.leftNegCount = toCopy.leftNegCount;
      this.rightPosCount = toCopy.rightPosCount;
      this.rightNegCount = toCopy.rightNegCount;
      this.leftMin = toCopy.leftMin;
      this.leftMax = toCopy.leftMax;
      this.rightMin = toCopy.rightMin;
      this.rightMax = toCopy.rightMax;
      assert(this.equals(toCopy));
    }
  
    public Set<Integer> getLeftIndices() {
      return leftIndices;
    }
  
    public Set<Integer> getRightIndices() {
      return rightIndices;
    }
  
    public double getLeftSum() {
      return leftSum;
    }
  
    public double getRightSum() {
      return rightSum;
    }
  
    public double getWeightedLeftSum() {
      return weightedLeftSum;
    }
  
    public double getWeightedRightSum() {
      return weightedRightSum;
    }
  
    public int getLeftPosCount() {
      return leftPosCount;
    }
  
    public int getLeftNegCount() {
      return leftNegCount;
    }
  
    public int getRightPosCount() {
      return rightPosCount;
    }
  
    public int getRightNegCount() {
      return rightNegCount;
    }
  
    public double getLeftMin() {
      return leftMin;
    }
  
    public double getLeftMax() {
      return leftMax;
    }
  
    public double getRightMin() {
      return rightMin;
    }
  
    public double getRightMax() {
      return rightMax;
    }
  
    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
  
      AtomSplit that = (AtomSplit) o;
  
      if (Double.compare(that.leftSum, leftSum) != 0) return false;
      if (Double.compare(that.rightSum, rightSum) != 0) return false;
      if (Double.compare(that.weightedLeftSum, weightedLeftSum) != 0) return false;
      if (Double.compare(that.weightedRightSum, weightedRightSum) != 0) return false;
      if (leftPosCount != that.leftPosCount) return false;
      if (leftNegCount != that.leftNegCount) return false;
      if (rightPosCount != that.rightPosCount) return false;
      if (rightNegCount != that.rightNegCount) return false;
      if (Double.compare(that.leftMin, leftMin) != 0) return false;
      if (Double.compare(that.leftMax, leftMax) != 0) return false;
      if (Double.compare(that.rightMin, rightMin) != 0) return false;
      if (Double.compare(that.rightMax, rightMax) != 0) return false;
      if (leftIndices != null ? !leftIndices.equals(that.leftIndices) : that.leftIndices != null) return false;
      return !(rightIndices != null ? !rightIndices.equals(that.rightIndices) : that.rightIndices != null);
  
    }
  
    @Override
    public int hashCode() {
      int result;
      long temp;
      result = leftIndices != null ? leftIndices.hashCode() : 0;
      result = 31 * result + (rightIndices != null ? rightIndices.hashCode() : 0);
      temp = Double.doubleToLongBits(leftSum);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(rightSum);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(weightedLeftSum);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(weightedRightSum);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      result = 31 * result + leftPosCount;
      result = 31 * result + leftNegCount;
      result = 31 * result + rightPosCount;
      result = 31 * result + rightNegCount;
      temp = Double.doubleToLongBits(leftMin);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(leftMax);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(rightMin);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      temp = Double.doubleToLongBits(rightMax);
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      return result;
    }
  
    public static Pair<AtomSplit, AtomSplit> computePlaneSplitsForIntersectingAtom(
        Collection<Integer> leftAtoms, Collection<Integer> rightAtoms,
        Integer planeIntersectingAtom, logPPlugin plugin,
        Map<Integer, Integer> surfaceComponentCounts) {
  
      /* Compute variants of the split with the current atom on either side of its plane.
       *
       * This isn't the most clever way to compute the optimal planes, since adjacent split points will end up creating
       * one redundant plane with each computation (if a and b are adjacent, a to the left of the border and b to the
       * right create the same split sets).  However, it's easy to reason about the correctness of this approach, so
       * we'll just live with the inefficiency for now.
       *
       * TODO: extend this to handle an arbitrary set of co-planar points that define the split, and apply the same
       * technique to all triples of atoms.
       * */
  
      AtomSplit ps = new AtomSplit();
      ps.leftIndices = new HashSet<>(leftAtoms);
      ps.rightIndices = new HashSet<>(rightAtoms);
  
      for (Integer a : leftAtoms) {
        if (planeIntersectingAtom.equals(a)) {
          // Skip the split point, though it could contribute to the "best" solution.
          continue;
        }
        Double logP = plugin.getAtomlogPIncrement(a);
        Double weightedLogP = logP * surfaceComponentCounts.get(a).doubleValue();
        ps.leftSum += logP;
        ps.weightedLeftSum += weightedLogP;
  
        if (logP < ps.leftMin) {
          ps.leftMin = logP;
        }
        if (logP > ps.leftMax) {
          ps.leftMax = logP;
        }
  
        if (logP >= 0.000) {
          ps.leftPosCount++;
        } else {
          ps.leftNegCount++;
        }
      }
      for (Integer a : rightAtoms) {
        if (planeIntersectingAtom.equals(a)) {
          continue;
        }
        Double logP = plugin.getAtomlogPIncrement(a);
        Double weightedLogP = logP * surfaceComponentCounts.get(a);
        ps.rightSum += logP;
        ps.weightedRightSum += weightedLogP;
  
        if (logP < ps.rightMin) {
          ps.rightMin = logP;
        }
        if (logP > ps.rightMax) {
          ps.rightMax = logP;
        }
  
        if (logP >= 0.000) {
          ps.rightPosCount++;
        } else {
          ps.rightNegCount++;
        }
      }
  
      // Create variants with this point added to left and right sides of split plane.
      AtomSplit leftVariant = ps;
      AtomSplit rightVariant = new AtomSplit(ps);
      Double logP = plugin.getAtomlogPIncrement(planeIntersectingAtom);
      Double weightedLogP = logP * surfaceComponentCounts.get(planeIntersectingAtom).doubleValue();
  
      leftVariant.leftIndices.add(planeIntersectingAtom);
      leftVariant.leftSum += logP;
      leftVariant.weightedLeftSum += weightedLogP;
      if (logP >= 0.000) {
        leftVariant.leftPosCount++;
      } else {
        leftVariant.leftNegCount++;
      }
      if (logP < leftVariant.leftMin) {
        leftVariant.leftMin = logP;
      }
      if (logP > leftVariant.leftMax) {
        leftVariant.leftMax = logP;
      }
  
  
      rightVariant.rightIndices.add(planeIntersectingAtom);
      rightVariant.rightSum += logP;
      rightVariant.weightedRightSum += weightedLogP;
      if (logP >= 0.000) {
        rightVariant.rightPosCount++;
      } else {
        rightVariant.rightNegCount++;
      }
      if (logP < rightVariant.rightMin) {
        rightVariant.rightMin = logP;
      }
      if (logP > rightVariant.rightMax) {
        rightVariant.rightMax = logP;
      }
  
      return Pair.of(leftVariant, rightVariant);
    }
  }
  