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.switching;
39  
40  import static java.lang.String.format;
41  import static org.apache.commons.math3.util.FastMath.abs;
42  import static org.apache.commons.math3.util.FastMath.max;
43  
44  import java.util.logging.Logger;
45  
46  /**
47   * The CompositeSwitch uses a primary switch in the middle, and then two secondary switches at the
48   * ends of the path to smoothly switch to the primary switch. For example, one can smoothly
49   * interpolate from 0/0/0 value/derivative/second derivative to a linear switch by multiplying the
50   * linear switch by a MultiplicativeSwitch in the range 0-0.1.
51   *
52   * <p>At present, there is an assumption that x gets linearly scaled when passed to the switch; at
53   * the ends, s(x) = f(g(x))*h(x), where h(x) is the primary switch, g(x) = x / (switching range), and
54   * f(g(x)) is the secondary switch.
55   *
56   * @author Jacob M. Litman
57   * @author Michael J. Schnieders
58   */
59  public class CompositeSwitch implements UnivariateSwitchingFunction {
60  
61    private static final Logger logger = Logger.getLogger(CompositeSwitch.class.getName());
62  
63    /**
64     * Primary switching function to be obeyed exactly in the middle.
65     */
66    private final UnivariateSwitchingFunction primaryFunction;
67    /**
68     * Secondary switching function used to interpolate smoothly (usually from 0/0/0) into
69     * primaryFunction over lb to lbPrimary.
70     */
71    private final UnivariateSwitchingFunction startSwitch;
72    /**
73     * Secondary switching function used to interpolate smoothly from primaryFunction (usually to
74     * 1/0/0) over lb to lbPrimary.
75     */
76    private final UnivariateSwitchingFunction endSwitch;
77    /**
78     * Lower bound of the primary switch/upper bound of startSwitch * primarySwitch.
79     */
80    private final double lbPrimary;
81    /**
82     * Upper bound of the primary switch/lower bound of endSwitch * primarySwitch.
83     */
84    private final double ubPrimary;
85    /**
86     * Overall lower bound of the CompositeSwitch.
87     */
88    private final double lb;
89    /**
90     * Overall upper bound of the CompositeSwitch.
91     */
92    private final double ub;
93  
94    // Below six constants are only valid if g(x) = x / range
95    // Precomputed constants used in evaluating values/derivatives from lb to lbPrimary.
96    private final double multLB;
97    private final double fdLB;
98    private final double fdLB2;
99    // Precomputed constants used in evaluating values/derivatives from ubPrimary to ub.
100   private final double multUB;
101   private final double fdUB;
102   private final double fdUB2;
103 
104   /**
105    * Builds a switch that uses MultiplicativeSwitches at the ends (0-0.1, 0.9-1.0) to smoothly
106    * interpolate a linear switch between 0 and 1 with smooth 2'nd and 3'rd derivatives. Will not be
107    * quite linear from 0-0.1 and 0.9-1.0.
108    */
109   public CompositeSwitch() {
110     this(new PowerSwitch());
111   }
112 
113   /**
114    * Builds a switch that uses MultiplicativeSwitches at the ends (0-0.1, 0.9-1.0) to smoothly
115    * interpolate a provided switch between 0 and 1 with smooth 2'nd and 3'rd derivatives.
116    *
117    * @param primary Primary switch to obey exactly from 0.1-0.9.
118    */
119   public CompositeSwitch(UnivariateSwitchingFunction primary) {
120     this(primary, new MultiplicativeSwitch(), new MultiplicativeSwitch(), 0.1, 0.9);
121   }
122 
123   /**
124    * Builds a composite switch in .
125    *
126    * @param primary   Primary switch to obey exactly from lbPrimary to ubPrimary.
127    * @param start     Switch to interpolate from 0 to primary between 0 and lbPrimary; assumed to
128    *                  internally function from 0-1.
129    * @param end       Switch to interpolate from primary to 1.0 between ubPrimary and 1; assumed to
130    *                  internally function from 0-1.
131    * @param lbPrimary Value at which primary should begin to be obeyed exactly.
132    * @param ubPrimary Value at which primary should stop being obeyed exactly.
133    */
134   public CompositeSwitch(UnivariateSwitchingFunction primary, UnivariateSwitchingFunction start,
135                          UnivariateSwitchingFunction end, double lbPrimary, double ubPrimary) {
136     this(primary, start, end, lbPrimary, ubPrimary, 0, 1);
137   }
138 
139   /**
140    * Builds a composite switch in .
141    *
142    * @param primary   Primary switch to obey exactly from lbPrimary to ubPrimary.
143    * @param start     Switch to interpolate from 0 to primary between 0 and lbPrimary; assumed to
144    *                  internally function from 0-1.
145    * @param end       Switch to interpolate from primary to 1.0 between ubPrimary and 1; assumed to
146    *                  internally function from 0-1.
147    * @param lbPrimary Value at which primary should begin to be obeyed exactly.
148    * @param ubPrimary Value at which primary should stop being obeyed exactly.
149    * @param ub        Overall upper bound of the CompositeSwitch.
150    * @param lb        Overall lower bound of the CompositeSwitch.
151    */
152   public CompositeSwitch(UnivariateSwitchingFunction primary, UnivariateSwitchingFunction start,
153                          UnivariateSwitchingFunction end, double lbPrimary, double ubPrimary, double lb, double ub) {
154     if (lbPrimary > ubPrimary) {
155       throw new IllegalArgumentException(
156           format(
157               " Lower primary bound %10.4g was greater than upper primary bound %10.4g",
158               lbPrimary, ubPrimary));
159     }
160     if (lb > ub) {
161       throw new IllegalArgumentException(
162           format(" Lower bound %10.4g was greater than upper bound %10.4g", lb, ub));
163     }
164     assert lb < lbPrimary && ub > ubPrimary;
165 
166     primaryFunction = primary;
167     startSwitch = start;
168     endSwitch = end;
169     this.lbPrimary = lbPrimary;
170     this.ubPrimary = ubPrimary;
171     this.lb = lb;
172     this.ub = ub;
173 
174     fdLB = lbPrimary - lb;
175     multLB = 1.0 / fdLB;
176     fdLB2 = fdLB * fdLB;
177 
178     fdUB = ub - ubPrimary;
179     multUB = 1.0 / fdUB;
180     fdUB2 = fdUB * fdUB;
181   }
182 
183   @Override
184   public boolean constantOutsideBounds() {
185     return startSwitch.constantOutsideBounds() && endSwitch.constantOutsideBounds();
186   }
187 
188   @Override
189   public double firstDerivative(double x) throws IllegalArgumentException {
190     if (x < lbPrimary) {
191       return fdLower(x);
192     } else if (x > ubPrimary) {
193       return fdUpper(x);
194     } else {
195       return primaryFunction.firstDerivative(x);
196     }
197   }
198 
199   @Override
200   public int getHighestOrderZeroDerivative() {
201     return Math.max(
202         Math.max(
203             startSwitch.getHighestOrderZeroDerivative(), endSwitch.getHighestOrderZeroDerivative()),
204         primaryFunction.getHighestOrderZeroDerivative());
205   }
206 
207   @Override
208   public double getOneBound() {
209     return ub;
210   }
211 
212   @Override
213   public double getZeroBound() {
214     return lb;
215   }
216 
217   @Override
218   public double nthDerivative(double x, int order) throws IllegalArgumentException {
219     return switch (order) {
220       case 0 -> valueAt(x);
221       case 1 -> firstDerivative(x);
222       case 2 -> secondDerivative(x);
223       default -> throw new IllegalArgumentException(
224           " Composite switches do not yet have support for arbitrary derivatives");
225     };
226   }
227 
228   @Override
229   public double secondDerivative(double x) throws IllegalArgumentException {
230     if (x < lbPrimary) {
231       return sdLower(x);
232     } else if (x > ubPrimary) {
233       return sdUpper(x);
234     } else {
235       return primaryFunction.secondDerivative(x);
236     }
237   }
238 
239   @Override
240   public boolean symmetricToUnity() {
241     return primaryFunction.symmetricToUnity()
242         && startSwitch.equals(endSwitch)
243         && (lbPrimary - lb == ub - ubPrimary);
244   }
245 
246   @Override
247   public String toString() {
248     StringBuilder sb = new StringBuilder(
249         format(" Composite switch with overall range %12.5g-%12.5g, "
250             + "with an inner range %12.5g-%12.5g", lb, ub, lbPrimary, ubPrimary));
251     sb.append("\n Primary switch: ").append(primaryFunction.toString());
252     sb.append("\n Start switch:   ").append(startSwitch.toString());
253     sb.append("\n End switch:     ").append(endSwitch.toString());
254     return sb.toString();
255   }
256 
257   @Override
258   public boolean validOutsideBounds() {
259     return startSwitch.constantOutsideBounds() && endSwitch.constantOutsideBounds();
260   }
261 
262   @Override
263   public double valueAt(double x) throws IllegalArgumentException {
264     if (x < lbPrimary) {
265       return valLower(x);
266     } else if (x > ubPrimary) {
267       return valUpper(x);
268     } else {
269       return primaryFunction.valueAt(x);
270     }
271   }
272 
273   private boolean approxEquals(double x1, double x2) {
274     return approxEquals(x1, x2, 1E-11);
275   }
276 
277   /**
278    * Tests double equality to within a reasonable tolerance.
279    *
280    * @param x1  First value.
281    * @param x2  Second value.
282    * @param tol Tolerance.
283    * @return Fuzzy equality.
284    */
285   private boolean approxEquals(double x1, double x2, double tol) {
286     double largerVal = max(abs(x1), abs(x2));
287     if (largerVal == 0) {
288       // If both are zero, they're both equal.
289       return true;
290     } else if (largerVal > 1E-6) {
291       // Use normalized approximate-equals.
292       return abs((x1 - x2) / largerVal) < tol;
293     } else {
294       // Use absolute approximate-equals to avoid numerical issues.
295       return abs(x1 - x2) < tol;
296     }
297   }
298 
299   /**
300    * Test that values, first derivatives, and second derivatives are smooth at lbPrimary and
301    * ubPrimary, such that the primary switch matches the composite switch at these values.
302    *
303    * @return If joints are smooth.
304    */
305   private boolean testJoints() {
306     if (!approxEquals(valLower(lbPrimary), primaryFunction.valueAt(lbPrimary))) {
307       return false;
308     }
309     if (!approxEquals(valUpper(ubPrimary), primaryFunction.valueAt(ubPrimary))) {
310       return false;
311     }
312 
313     if (!approxEquals(fdLower(lbPrimary), primaryFunction.firstDerivative(lbPrimary))) {
314       return false;
315     }
316     if (!approxEquals(fdUpper(ubPrimary), primaryFunction.firstDerivative(ubPrimary))) {
317       return false;
318     }
319 
320     if (!approxEquals(sdLower(lbPrimary), primaryFunction.secondDerivative(lbPrimary))) {
321       return false;
322     }
323     return approxEquals(sdUpper(ubPrimary), primaryFunction.secondDerivative(ubPrimary));
324   }
325 
326   private double lbX(double x) {
327     return (x - lb) * multLB;
328   }
329 
330   private double ubX(double x) {
331     return (ub - x) * multUB;
332   }
333 
334   /**
335    * Broken out from valueAt to make testing joints easier.
336    *
337    * @param x x value to evaluate.
338    * @return f(x) using both startSwitch and primary function.
339    */
340   private double valLower(double x) {
341     return startSwitch.valueAt(lbX(x)) * primaryFunction.valueAt(x);
342   }
343 
344   /**
345    * Broken out from valueAt to make testing joints easier.
346    *
347    * @param x x value to evaluate.
348    * @return f(x) using both endSwitch and primary function.
349    */
350   private double valUpper(double x) {
351     return endSwitch.valueAt(ubX(x)) * primaryFunction.valueAt(x);
352   }
353 
354   private double fdLower(double x) {
355     double swX = lbX(x);
356     double val = primaryFunction.firstDerivative(x) * startSwitch.valueAt(swX);
357     val += primaryFunction.valueAt(x) * startSwitch.firstDerivative(swX) * fdLB;
358     return val;
359   }
360 
361   private double fdUpper(double x) {
362     double swX = ubX(x);
363     double val = primaryFunction.firstDerivative(x) * endSwitch.valueAt(swX);
364     val += primaryFunction.valueAt(x) * endSwitch.firstDerivative(swX) * fdUB;
365     return val;
366   }
367 
368   private double sdLower(double x) {
369     double swX = lbX(x);
370     double val = primaryFunction.secondDerivative(x) * startSwitch.valueAt(swX);
371     val += (2 * primaryFunction.firstDerivative(x) * startSwitch.firstDerivative(swX) * fdLB);
372     val += (primaryFunction.valueAt(x) * startSwitch.secondDerivative(swX) * fdLB2);
373     return val;
374   }
375 
376   private double sdUpper(double x) {
377     double swX = ubX(x);
378     double val = primaryFunction.secondDerivative(x) * endSwitch.valueAt(swX);
379     val += (2 * primaryFunction.firstDerivative(x) * endSwitch.firstDerivative(swX) * fdUB);
380     val += (primaryFunction.valueAt(x) * endSwitch.secondDerivative(swX) * fdUB2);
381     return val;
382   }
383 }