View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.numerics.clustering;
39  
40  import java.util.ArrayList;
41  import java.util.List;
42  
43  /**
44   * Represents a node in a hierarchical clustering tree (dendrogram).
45   * A Cluster may be a leaf (no children) or an internal node with two children.
46   * It tracks its name, parent, children, the list of leaf names contained beneath it,
47   * and an associated Distance used to store linkage distance and aggregate weight.
48   *
49   * @author Lars Behnke, 2013
50   * @author Michael J. Schnieders
51   * @since 1.0
52   */
53  public class Cluster {
54  
55    private String name;
56  
57    private Cluster parent;
58  
59    private List<Cluster> children;
60  
61    private final List<String> leafNames;
62  
63    private Distance distance = new Distance();
64  
65  
66    /**
67     * Creates a new Cluster with the provided name.
68     *
69     * @param name the cluster name
70     */
71    public Cluster(String name) {
72      this.name = name;
73      leafNames = new ArrayList<>();
74    }
75  
76    /**
77     * Gets the Distance metadata for this cluster (linkage distance and weight).
78     *
79     * @return the Distance object
80     */
81    public Distance getDistance() {
82      return distance;
83    }
84  
85    /**
86     * Convenience accessor for this cluster's aggregate weight.
87     *
88     * @return the weight value stored in the Distance
89     */
90    public Double getWeightValue() {
91      return distance.getWeight();
92    }
93  
94    /**
95     * Convenience accessor for this cluster's linkage distance value.
96     *
97     * @return the linkage distance, or null if unset
98     */
99    public Double getDistanceValue() {
100     return distance.getDistance();
101   }
102 
103   /**
104    * Sets the Distance metadata for this cluster.
105    *
106    * @param distance the Distance to set
107    */
108   public void setDistance(Distance distance) {
109     this.distance = distance;
110   }
111 
112   /**
113    * Returns the list of child clusters, creating it lazily if needed.
114    *
115    * @return mutable list of child clusters (possibly empty)
116    */
117   public List<Cluster> getChildren() {
118     if (children == null) {
119       children = new ArrayList<>();
120     }
121 
122     return children;
123   }
124 
125   /**
126    * Adds a single leaf name contained within this cluster's subtree.
127    *
128    * @param lname the leaf name to add
129    */
130   public void addLeafName(String lname) {
131     leafNames.add(lname);
132   }
133 
134   /**
135    * Appends a list of leaf names to this cluster's leaf name list.
136    *
137    * @param lnames list of leaf names to append
138    */
139   public void appendLeafNames(List<String> lnames) {
140     leafNames.addAll(lnames);
141   }
142 
143   /**
144    * Returns the list of leaf names beneath this cluster.
145    *
146    * @return list of leaf names
147    */
148   public List<String> getLeafNames() {
149     return leafNames;
150   }
151 
152   /**
153    * Sets the list of child clusters for this node.
154    *
155    * @param children the children to set
156    */
157   public void setChildren(List<Cluster> children) {
158     this.children = children;
159   }
160 
161   /**
162    * Gets the parent cluster of this node, or null if this is the root.
163    *
164    * @return the parent cluster, or null
165    */
166   public Cluster getParent() {
167     return parent;
168   }
169 
170   /**
171    * Sets the parent cluster of this node.
172    *
173    * @param parent the parent cluster to set
174    */
175   public void setParent(Cluster parent) {
176     this.parent = parent;
177   }
178 
179 
180   /**
181    * Gets the name of this cluster.
182    *
183    * @return the cluster name
184    */
185   public String getName() {
186     return name;
187   }
188 
189   /**
190    * Sets the name of this cluster.
191    *
192    * @param name the name to set
193    */
194   public void setName(String name) {
195     this.name = name;
196   }
197 
198   /**
199    * Adds a child cluster to this node.
200    *
201    * @param cluster the child to add
202    */
203   public void addChild(Cluster cluster) {
204     getChildren().add(cluster);
205 
206   }
207 
208   /**
209    * Tests whether this cluster has the specified child.
210    *
211    * @param cluster the child to look for
212    * @return true if present; false otherwise
213    */
214   public boolean contains(Cluster cluster) {
215     return getChildren().contains(cluster);
216   }
217 
218   @Override
219   public String toString() {
220     return "Cluster " + name;
221   }
222 
223   @Override
224   public boolean equals(Object obj) {
225     if (this == obj) {
226       return true;
227     }
228     if (obj == null) {
229       return false;
230     }
231     if (getClass() != obj.getClass()) {
232       return false;
233     }
234     Cluster other = (Cluster) obj;
235     if (name == null) {
236       return other.name == null;
237     } else return name.equals(other.name);
238   }
239 
240   @Override
241   public int hashCode() {
242     return (name == null) ? 0 : name.hashCode();
243   }
244 
245   /**
246    * Returns true if this cluster is a leaf (has no children).
247    *
248    * @return true if leaf; false otherwise
249    */
250   public boolean isLeaf() {
251     return getChildren().isEmpty();
252   }
253 
254   /**
255    * Counts the number of leaf descendants beneath this cluster.
256    *
257    * @return number of leaf nodes
258    */
259   public int countLeafs() {
260     return countLeafs(this, 0);
261   }
262 
263   /**
264    * Recursive helper to count leaves under the specified node.
265    *
266    * @param node  the node to inspect
267    * @param count running count of leaves
268    * @return updated count including leaves beneath node
269    */
270   public int countLeafs(Cluster node, int count) {
271     if (node.isLeaf()) count++;
272     for (Cluster child : node.getChildren()) {
273       count += child.countLeafs();
274     }
275     return count;
276   }
277 
278   /**
279    * Prints this cluster and its subtree to the console with indentation.
280    *
281    * @param indent number of indentation levels for this node
282    */
283   public void toConsole(int indent) {
284     for (int i = 0; i < indent; i++) {
285       System.out.print("  ");
286 
287     }
288     String name = getName() + (isLeaf() ? " (leaf)" : "") + (distance != null ? "  distance: " + distance : "");
289     System.out.println(name);
290     for (Cluster child : getChildren()) {
291       child.toConsole(indent + 1);
292     }
293   }
294 
295   /**
296    * Serializes this cluster subtree into a simple Newick-like string.
297    * The first child is annotated with distance, the second with weight.
298    *
299    * @param indent indentation spaces to include (for readability)
300    * @return a Newick-like representation of this subtree
301    */
302   public String toNewickString(int indent) {
303     StringBuilder cdtString = new StringBuilder();
304     if (!isLeaf()) cdtString.append("(");
305 
306     cdtString.append(" ".repeat(Math.max(0, indent)));
307 
308 
309     if (isLeaf()) {
310       cdtString.append(getName());
311     }
312 
313     List<Cluster> children = getChildren();
314 
315     boolean firstChild = true;
316     for (Cluster child : children) {
317       cdtString.append(child.toNewickString(indent));
318       String distanceString = distance.getDistance().toString().replace(",", ".");
319       String weightString = distance.getWeight().toString().replace(",", ".");
320       if (firstChild) cdtString.append(":").append(distanceString).append(",");
321       else cdtString.append(":").append(weightString);
322 
323       firstChild = false;
324     }
325 
326     cdtString.append(" ".repeat(Math.max(0, indent)));
327 
328     if (!isLeaf()) cdtString.append(")");
329 
330     return cdtString.toString();
331   }
332 
333   /**
334    * Computes the cumulative distance down the leftmost branch of this cluster.
335    *
336    * @return total distance along the first-child path
337    */
338   public double getTotalDistance() {
339     double dist = getDistance() == null ? 0 : getDistance().getDistance();
340     if (!getChildren().isEmpty()) {
341       dist += children.getFirst().getTotalDistance();
342     }
343     return dist;
344 
345   }
346 
347 }