View Javadoc
1   /*
2    * Licensed to the Hipparchus project under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The Hipparchus project licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.hipparchus.analysis.polynomials;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.Field;
21  import org.hipparchus.exception.LocalizedCoreFormats;
22  import org.hipparchus.exception.MathIllegalArgumentException;
23  import org.hipparchus.exception.NullArgumentException;
24  import org.hipparchus.util.MathArrays;
25  
26  /**
27   * Smoothstep function factory.
28   * <p>
29   * It allows for quick creation of common and generic smoothstep functions as defined
30   * <a href="https://en.wikipedia.org/wiki/Smoothstep">here</a>.
31   */
32  public class SmoothStepFactory {
33  
34      /**
35       * Private constructor.
36       * <p>
37       * This class is a utility class, it should neither have a public nor a default constructor. This private constructor
38       * prevents the compiler from generating one automatically.
39       */
40      private SmoothStepFactory() {
41          // Empty constructor
42      }
43  
44      /**
45       * Get the {@link SmoothStepFunction clamping smoothstep function}.
46       *
47       * @return clamping smoothstep function
48       */
49      public static SmoothStepFunction getClamp() {
50          return getGeneralOrder(0);
51      }
52  
53      /**
54       * Get the {@link SmoothStepFunction quadratic smoothstep function}.
55       *
56       * @return clamping smoothstep function
57       */
58      public static SmoothStepFunction getQuadratic() {
59          // Use a default double array as it will not matter anyway
60          return new QuadraticSmoothStepFunction(new double[] { 0 });
61      }
62  
63      /**
64       * Get the {@link SmoothStepFunction cubic smoothstep function}.
65       *
66       * @return cubic smoothstep function
67       */
68      public static SmoothStepFunction getCubic() {
69          return getGeneralOrder(1);
70      }
71  
72      /**
73       * Get the {@link SmoothStepFunction quintic smoothstep function}.
74       *
75       * @return quintic smoothstep function
76       */
77      public static SmoothStepFunction getQuintic() {
78          return getGeneralOrder(2);
79      }
80  
81      /**
82       * Get the {@link SmoothStepFunction clamping smoothstep function}.
83       *
84       * @param <T> type of the field element
85       * @param field field of the element
86       *
87       * @return clamping smoothstep function
88       */
89      public static <T extends CalculusFieldElement<T>> FieldSmoothStepFunction<T> getClamp(final Field<T> field) {
90          return getFieldGeneralOrder(field, 0);
91      }
92  
93      /**
94       * Get the {@link SmoothStepFunction quadratic smoothstep function}.
95       *
96       * @param <T> type of the field element
97       * @param field field of the element
98       *
99       * @return clamping smoothstep function
100      */
101     public static <T extends CalculusFieldElement<T>> FieldSmoothStepFunction<T> getQuadratic(final Field<T> field) {
102         final T[] tempArray = MathArrays.buildArray(field, 1);
103         return new FieldQuadraticSmoothStepFunction<>(tempArray);
104     }
105 
106     /**
107      * Get the {@link SmoothStepFunction cubic smoothstep function}.
108      *
109      * @param <T> type of the field element
110      * @param field field of the element
111      *
112      * @return cubic smoothstep function
113      */
114     public static <T extends CalculusFieldElement<T>> FieldSmoothStepFunction<T> getCubic(final Field<T> field) {
115         return getFieldGeneralOrder(field, 1);
116     }
117 
118     /**
119      * Get the {@link SmoothStepFunction quintic smoothstep function}.
120      *
121      * @param <T> type of the field element
122      * @param field field of the element
123      *
124      * @return quintic smoothstep function
125      */
126     public static <T extends CalculusFieldElement<T>> FieldSmoothStepFunction<T> getQuintic(final Field<T> field) {
127         return getFieldGeneralOrder(field, 2);
128     }
129 
130     /**
131      * Create a {@link SmoothStepFunction smoothstep function} of order <b>2N + 1</b>.
132      * <p>
133      * It uses the general smoothstep equation presented <a href="https://en.wikipedia.org/wiki/Smoothstep">here</a> :
134      * $S_{N}(x) = \sum_{n=0}^{N} \begin{pmatrix} -N-1 \\ n \end{pmatrix} \begin{pmatrix} 2N+1 \\ N-n \end{pmatrix}
135      * x^{N+n+1}$
136      *
137      * @param N determines the order of the output smoothstep function (=2N + 1)
138      *
139      * @return smoothstep function of order <b>2N + 1</b>
140      */
141     public static SmoothStepFunction getGeneralOrder(final int N) {
142 
143         final int twoNPlusOne = 2 * N + 1;
144 
145         final double[] coefficients = new double[twoNPlusOne + 1];
146 
147         int n = N;
148         for (int i = twoNPlusOne; i > N; i--) {
149             coefficients[i] = pascalTriangle(-N - 1, n) * pascalTriangle(2 * N + 1, N - n);
150             n--;
151         }
152 
153         return new SmoothStepFunction(coefficients);
154     }
155 
156     /**
157      * Create a {@link SmoothStepFunction smoothstep function} of order <b>2N + 1</b>.
158      * <p>
159      * It uses the general smoothstep equation presented <a href="https://en.wikipedia.org/wiki/Smoothstep">here</a> :
160      * $S_{N}(x) = \sum_{n=0}^{N} \begin{pmatrix} -N-1 \\ n \end{pmatrix} \begin{pmatrix} 2N+1 \\ N-n \end{pmatrix}
161      * x^{N+n+1}$
162      *
163      * @param <T> type of the field element
164      * @param field field of the element
165      * @param N determines the order of the output smoothstep function (=2N + 1)
166      *
167      * @return smoothstep function of order <b>2N + 1</b>
168      */
169     public static <T extends CalculusFieldElement<T>> FieldSmoothStepFunction<T> getFieldGeneralOrder(final Field<T> field,
170                                                                                                       final int N) {
171 
172         final int twoNPlusOne = 2 * N + 1;
173 
174         final T[] coefficients = MathArrays.buildArray(field, twoNPlusOne + 1);
175 
176         final T one = field.getOne();
177         int     n   = N;
178         for (int i = twoNPlusOne; i > N; i--) {
179             coefficients[i] = one.newInstance(pascalTriangle(-N - 1, n) * pascalTriangle(2 * N + 1, N - n));
180             n--;
181         }
182 
183         return new FieldSmoothStepFunction<>(coefficients);
184     }
185 
186     /**
187      * Returns binomial coefficient without explicit use of factorials, which can't be used with negative integers
188      *
189      * @param k subset in set
190      * @param n set
191      *
192      * @return number of subset {@code k} in global set {@code n}
193      */
194     private static int pascalTriangle(final int k, final int n) {
195 
196         int result = 1;
197         for (int i = 0; i < n; i++) {
198             result *= (k - i) / (i + 1);
199         }
200 
201         return result;
202     }
203 
204     /**
205      * Check that input is between [0:1].
206      *
207      * @param input input to be checked
208      *
209      * @throws MathIllegalArgumentException if input is not between [0:1]
210      */
211     public static void checkBetweenZeroAndOneIncluded(final double input) throws MathIllegalArgumentException {
212         if (input < 0 || input > 1) {
213             throw new MathIllegalArgumentException(
214                     LocalizedCoreFormats.INPUT_EXPECTED_BETWEEN_ZERO_AND_ONE_INCLUDED);
215         }
216     }
217 
218     /**
219      * Smoothstep function as defined <a href="https://en.wikipedia.org/wiki/Smoothstep">here</a>.
220      * <p>
221      * It is used to do a smooth transition between the "left edge" and the "right edge" with left edge assumed to be smaller
222      * than right edge.
223      * <p>
224      * By definition, for order n greater than 1 and input x, a smoothstep function respects at least the following properties :
225      * <ul>
226      *     <li>f(x &lt;= leftEdge) = 0 and f(x &gt;= rightEdge) = 1</li>
227      *     <li>f'(leftEdge) = f'(rightEdge) = 0</li>
228      * </ul>
229      * If x is normalized between edges, we have at least :
230      * <ul>
231      *     <li>f(x &lt;= 0) = 0 and f(x &gt;= 1) = 1</li>
232      *     <li>f'(0) = f'(1) = 0</li>
233      * </ul>
234      * Smoothstep functions of higher order n will have their higher time derivatives also equal to zero at edges...
235      */
236     public static class SmoothStepFunction extends PolynomialFunction {
237 
238         /** Serializable UID. */
239         private static final long serialVersionUID = 20230113L;
240 
241         /**
242          * Construct a smoothstep with the given coefficients. The first element of the coefficients array is the constant
243          * term.  Higher degree coefficients follow in sequence.  The degree of the resulting polynomial is the index of the
244          * last non-null element of the array, or 0 if all elements are null.
245          * <p>
246          * The constructor makes a copy of the input array and assigns the copy to the coefficients property.</p>
247          *
248          * @param c Smoothstep polynomial coefficients.
249          *
250          * @throws NullArgumentException        if {@code c} is {@code null}.
251          * @throws MathIllegalArgumentException if {@code c} is empty.
252          */
253         private SmoothStepFunction(final double[] c) throws MathIllegalArgumentException, NullArgumentException {
254             super(c);
255         }
256 
257         /**
258          * Compute the value of the smoothstep for the given argument normalized between edges.
259          *
260          * @param xNormalized Normalized argument for which the function value should be computed. It is expected to be
261          * between [0:1] and will throw an exception otherwise.
262          *
263          * @return the value of the polynomial at the given point.
264          *
265          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
266          */
267         @Override
268         public double value(final double xNormalized) {
269             checkBetweenZeroAndOneIncluded(xNormalized);
270             return super.value(xNormalized);
271         }
272 
273         /**
274          * Compute the value of the smoothstep function for the given edges and argument.
275          * <p>
276          * Note that right edge is expected to be greater than left edge. It will throw an exception otherwise.
277          *
278          * @param leftEdge left edge
279          * @param rightEdge right edge
280          * @param x Argument for which the function value should be computed
281          *
282          * @return the value of the polynomial at the given point
283          *
284          * @throws MathIllegalArgumentException if right edge is greater than left edge
285          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
286          */
287         public double value(final double leftEdge, final double rightEdge, final double x)
288                 throws MathIllegalArgumentException {
289 
290             checkInputEdges(leftEdge, rightEdge);
291 
292             final double xClamped = clampInput(leftEdge, rightEdge, x);
293 
294             final double xNormalized = normalizeInput(leftEdge, rightEdge, xClamped);
295 
296             return super.value(xNormalized);
297         }
298 
299         /**
300          * Check that left edge is lower than right edge. Otherwise, throw an exception.
301          *
302          * @param leftEdge left edge
303          * @param rightEdge right edge
304          */
305         protected void checkInputEdges(final double leftEdge, final double rightEdge) {
306             if (leftEdge > rightEdge) {
307                 throw new MathIllegalArgumentException(LocalizedCoreFormats.RIGHT_EDGE_GREATER_THAN_LEFT_EDGE,
308                                                        leftEdge, rightEdge);
309             }
310         }
311 
312         /**
313          * Clamp input between edges.
314          *
315          * @param leftEdge left edge
316          * @param rightEdge right edge
317          * @param x input to clamp
318          *
319          * @return clamped input
320          */
321         protected double clampInput(final double leftEdge, final double rightEdge, final double x) {
322             if (x <= leftEdge) {
323                 return leftEdge;
324             }
325             if (x >= rightEdge) {
326                 return rightEdge;
327             }
328             return x;
329         }
330 
331         /**
332          * Normalize input between left and right edges.
333          *
334          * @param leftEdge left edge
335          * @param rightEdge right edge
336          * @param x input to normalize
337          *
338          * @return normalized input
339          */
340         protected double normalizeInput(final double leftEdge, final double rightEdge, final double x) {
341             return (x - leftEdge) / (rightEdge - leftEdge);
342         }
343     }
344 
345     /**
346      * Specific smoothstep function that cannot be built using the {@link #getGeneralOrder(int)}.
347      * <p>
348      * Methods inherited from {@link PolynomialFunction} <em>should not be used</em> as they will not be true to the actual
349      * function.
350      *
351      * @see PolynomialFunction
352      */
353     public static class QuadraticSmoothStepFunction extends SmoothStepFunction {
354 
355         /** Serializable UID. */
356         private static final long serialVersionUID = 20230422L;
357 
358         /**
359          * Construct a smoothstep with the given coefficients. The first element of the coefficients array is the constant
360          * term. Higher degree coefficients follow in sequence.  The degree of the resulting polynomial is the index of the
361          * last non-null element of the array, or 0 if all elements are null.
362          * <p>
363          * The constructor makes a copy of the input array and assigns the copy to the coefficients property.</p>
364          *
365          * @param c Smoothstep polynomial coefficients.
366          *
367          * @throws NullArgumentException        if {@code c} is {@code null}.
368          * @throws MathIllegalArgumentException if {@code c} is empty.
369          */
370         private QuadraticSmoothStepFunction(final double[] c) throws MathIllegalArgumentException, NullArgumentException {
371             super(c);
372         }
373 
374         /**
375          * Compute the value of the smoothstep function for the given edges and argument.
376          * <p>
377          * Note that right edge is expected to be greater than left edge. It will throw an exception otherwise.
378          *
379          * @param leftEdge left edge
380          * @param rightEdge right edge
381          * @param x Argument for which the function value should be computed
382          *
383          * @return the value of the polynomial at the given point
384          *
385          * @throws MathIllegalArgumentException if right edge is greater than left edge
386          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
387          */
388         @Override
389         public double value(final double leftEdge, final double rightEdge, final double x)
390                 throws MathIllegalArgumentException {
391 
392             checkInputEdges(leftEdge, rightEdge);
393 
394             final double xClamped = clampInput(leftEdge, rightEdge, x);
395 
396             final double xNormalized = normalizeInput(leftEdge, rightEdge, xClamped);
397 
398             return value(xNormalized);
399         }
400 
401         /**
402          * Compute the value of the quadratic smoothstep for the given argument normalized between edges.
403          *
404          * @param xNormalized Normalized argument for which the function value should be computed. It is expected to be
405          * between [0:1] and will throw an exception otherwise.
406          *
407          * @return the value of the polynomial at the given point.
408          *
409          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
410          */
411         @Override
412         public double value(final double xNormalized) {
413             checkBetweenZeroAndOneIncluded(xNormalized);
414 
415             if (xNormalized >= 0 && xNormalized <= 0.5) {
416                 return 2 * xNormalized * xNormalized;
417             }
418             else {
419                 return 4 * xNormalized - 2 * xNormalized * xNormalized - 1;
420             }
421         }
422     }
423 
424     /**
425      * Smoothstep function as defined <a href="https://en.wikipedia.org/wiki/Smoothstep">here</a>.
426      * <p>
427      * It is used to do a smooth transition between the "left edge" and the "right edge" with left edge assumed to be smaller
428      * than right edge.
429      * <p>
430      * By definition, for order n greater than 1 and input x, a smoothstep function respects at least the following properties :
431      * <ul>
432      *     <li>f(x &lt;= leftEdge) = 0 and f(x &gt;= rightEdge) = 1</li>
433      *     <li>f'(leftEdge) = f'(rightEdge) = 0</li>
434      * </ul>
435      * If x is normalized between edges, we have at least :
436      * <ul>
437      *     <li>f(x &lt;= 0) = 0 and f(x &gt;= 1) = 1</li>
438      *     <li>f'(0) = f'(1) = 0</li>
439      * </ul>
440      * Smoothstep functions of higher order n will have their higher time derivatives also equal to zero at edges...
441      *
442      * @param <T> type of the field element
443      */
444     public static class FieldSmoothStepFunction<T extends CalculusFieldElement<T>> extends FieldPolynomialFunction<T> {
445 
446         /**
447          * Construct a smoothstep with the given coefficients. The first element of the coefficients array is the constant
448          * term.  Higher degree coefficients follow in sequence.  The degree of the resulting polynomial is the index of the
449          * last non-null element of the array, or 0 if all elements are null.
450          * <p>
451          * The constructor makes a copy of the input array and assigns the copy to the coefficients property.</p>
452          *
453          * @param c Smoothstep polynomial coefficients.
454          *
455          * @throws NullArgumentException        if {@code c} is {@code null}.
456          * @throws MathIllegalArgumentException if {@code c} is empty.
457          */
458         private FieldSmoothStepFunction(final T[] c) throws MathIllegalArgumentException, NullArgumentException {
459             super(c);
460         }
461 
462         /**
463          * Compute the value of the smoothstep for the given argument normalized between edges.
464          *
465          * @param xNormalized Normalized argument for which the function value should be computed. It is expected to be
466          * between [0:1] and will throw an exception otherwise.
467          *
468          * @return the value of the polynomial at the given point.
469          *
470          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
471          */
472         @Override
473         public T value(final double xNormalized) {
474             checkBetweenZeroAndOneIncluded(xNormalized);
475             return super.value(xNormalized);
476         }
477 
478         /**
479          * Compute the value of the smoothstep for the given argument normalized between edges.
480          *
481          * @param xNormalized Normalized argument for which the function value should be computed. It is expected to be
482          * between [0:1] and will throw an exception otherwise.
483          *
484          * @return the value of the polynomial at the given point.
485          *
486          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
487          */
488         @Override
489         public T value(final T xNormalized) {
490             checkBetweenZeroAndOneIncluded(xNormalized.getReal());
491             return super.value(xNormalized);
492         }
493 
494         /**
495          * Compute the value of the smoothstep function for the given edges and argument.
496          * <p>
497          * Note that right edge is expected to be greater than left edge. It will throw an exception otherwise.
498          *
499          * @param leftEdge left edge
500          * @param rightEdge right edge
501          * @param x Argument for which the function value should be computed
502          *
503          * @return the value of the polynomial at the given point
504          *
505          * @throws MathIllegalArgumentException if right edge is greater than left edge
506          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
507          */
508         public T value(final double leftEdge, final double rightEdge, final T x)
509                 throws MathIllegalArgumentException {
510 
511             checkInputEdges(leftEdge, rightEdge);
512 
513             final T xClamped = clampInput(leftEdge, rightEdge, x);
514 
515             final T xNormalized = normalizeInput(leftEdge, rightEdge, xClamped);
516 
517             return super.value(xNormalized);
518         }
519 
520         /**
521          * Check that left edge is lower than right edge. Otherwise, throw an exception.
522          *
523          * @param leftEdge left edge
524          * @param rightEdge right edge
525          */
526         protected void checkInputEdges(final double leftEdge, final double rightEdge) {
527             if (leftEdge > rightEdge) {
528                 throw new MathIllegalArgumentException(LocalizedCoreFormats.RIGHT_EDGE_GREATER_THAN_LEFT_EDGE,
529                                                        leftEdge, rightEdge);
530             }
531         }
532 
533         /**
534          * Clamp input between edges.
535          *
536          * @param leftEdge left edge
537          * @param rightEdge right edge
538          * @param x input to clamp
539          *
540          * @return clamped input
541          */
542         protected T clampInput(final double leftEdge, final double rightEdge, final T x) {
543             if (x.getReal() <= leftEdge) {
544                 return x.getField().getOne().newInstance(leftEdge);
545             }
546             if (x.getReal() >= rightEdge) {
547                 return x.getField().getOne().newInstance(rightEdge);
548             }
549             return x;
550         }
551 
552         /**
553          * Normalize input between left and right edges.
554          *
555          * @param leftEdge left edge
556          * @param rightEdge right edge
557          * @param x input to normalize
558          *
559          * @return normalized input
560          */
561         protected T normalizeInput(final double leftEdge, final double rightEdge, final T x) {
562             return x.subtract(leftEdge).divide(rightEdge - leftEdge);
563         }
564     }
565 
566     /**
567      * Specific smoothstep function that cannot be built using the {@link #getGeneralOrder(int)}.
568      * <p>
569      * Methods inherited from {@link PolynomialFunction} <em>should not be used</em> as they will not be true to the actual
570      * function.
571      *
572      * @param <T> type of the field element
573      *
574      * @see PolynomialFunction
575      */
576     private static class FieldQuadraticSmoothStepFunction<T extends CalculusFieldElement<T>>
577             extends FieldSmoothStepFunction<T> {
578 
579         /**
580          * Construct a smoothstep with the given coefficients. The first element of the coefficients array is the constant
581          * term. Higher degree coefficients follow in sequence.  The degree of the resulting polynomial is the index of the
582          * last non-null element of the array, or 0 if all elements are null.
583          * <p>
584          * The constructor makes a copy of the input array and assigns the copy to the coefficients property.</p>
585          *
586          * @param c Smoothstep polynomial coefficients.
587          *
588          * @throws NullArgumentException        if {@code c} is {@code null}.
589          * @throws MathIllegalArgumentException if {@code c} is empty.
590          */
591         private FieldQuadraticSmoothStepFunction(final T[] c) throws MathIllegalArgumentException, NullArgumentException {
592             super(c);
593         }
594 
595         /**
596          * Compute the value of the smoothstep function for the given edges and argument.
597          * <p>
598          * Note that right edge is expected to be greater than left edge. It will throw an exception otherwise.
599          *
600          * @param leftEdge left edge
601          * @param rightEdge right edge
602          * @param x Argument for which the function value should be computed
603          *
604          * @return the value of the polynomial at the given point
605          *
606          * @throws MathIllegalArgumentException if right edge is greater than left edge
607          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
608          */
609         @Override
610         public T value(final double leftEdge, final double rightEdge, final T x)
611                 throws MathIllegalArgumentException {
612 
613             checkInputEdges(leftEdge, rightEdge);
614 
615             final T xClamped = clampInput(leftEdge, rightEdge, x);
616 
617             final T xNormalized = normalizeInput(leftEdge, rightEdge, xClamped);
618 
619             return value(xNormalized);
620         }
621 
622         /**
623          * Compute the value of the quadratic smoothstep for the given argument normalized between edges.
624          * <p>
625          *
626          * @param xNormalized Normalized argument for which the function value should be computed. It is expected to be
627          * between [0:1] and will throw an exception otherwise.
628          *
629          * @return the value of the polynomial at the given point.
630          *
631          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
632          */
633         @Override
634         public T value(final double xNormalized) {
635             checkBetweenZeroAndOneIncluded(xNormalized);
636 
637             final Field<T> field = getField();
638             final T        one   = field.getOne();
639 
640             if (xNormalized >= 0 && xNormalized <= 0.5) {
641                 return one.newInstance(2. * xNormalized * xNormalized);
642             }
643             else {
644                 return one.newInstance(4. * xNormalized - 2. * xNormalized * xNormalized - 1.);
645             }
646         }
647 
648         /**
649          * Compute the value of the quadratic smoothstep for the given argument normalized between edges.
650          * <p>
651          *
652          * @param xNormalized Normalized argument for which the function value should be computed. It is expected to be
653          * between [0:1] and will throw an exception otherwise.
654          *
655          * @return the value of the polynomial at the given point.
656          *
657          * @see org.hipparchus.analysis.UnivariateFunction#value(double)
658          */
659         @Override
660         public T value(final T xNormalized) {
661             checkBetweenZeroAndOneIncluded(xNormalized.getReal());
662 
663             if (xNormalized.getReal() >= 0 && xNormalized.getReal() <= 0.5) {
664                 return xNormalized.multiply(xNormalized).multiply(2.);
665             }
666             else {
667                 final T one = getField().getOne();
668                 return one.linearCombination(4., xNormalized, -2., xNormalized.multiply(xNormalized)).subtract(1.);
669             }
670         }
671     }
672 
673 }