View Javadoc
1   package org.djutils.math.functions;
2   
3   import java.util.ArrayList;
4   import java.util.Arrays;
5   import java.util.Collections;
6   import java.util.List;
7   import java.util.Objects;
8   import java.util.SortedSet;
9   import java.util.TreeSet;
10  
11  import org.djutils.exceptions.Throw;
12  
13  /**
14   * Multiply functions.
15   * <p>
16   * Copyright (c) 2024-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
17   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
18   * distributed under a three-clause BSD-style license, which can be found at
19   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
20   * </p>
21   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
22   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
23   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
24   */
25  public class Product implements MathFunction
26  {
27      /** The functions whose values will be summed. */
28      private final List<MathFunction> factors;
29  
30      /**
31       * Construct the product of one or more functions.
32       * @param functions the functions that this Product will multiply together.
33       * @throws IllegalArgumentException when zero parameters are provided
34       * @throws NullPointerException when a <code>null</code> value is among the arguments
35       */
36      public Product(final MathFunction... functions)
37      {
38          this(Arrays.asList(functions));
39      }
40  
41      /**
42       * Construct the product of one or more functions.
43       * @param functions the functions that this Product will multiply together.
44       * @throws IllegalArgumentException when zero parameters are provided
45       * @throws NullPointerException when a <code>null</code> value is among the arguments
46       */
47      public Product(final List<MathFunction> functions)
48      {
49          Throw.when(functions.size() == 0, IllegalArgumentException.class, "Product needs at least one object to multiply");
50          this.factors = simplify(functions);
51      }
52  
53      /**
54       * Simplify a set of factors that must be multiplied together.
55       * @param functions the factors that must be multiplied together
56       * @return minimal array with the remaining factors
57       */
58      private List<MathFunction> simplify(final List<MathFunction> functions)
59      {
60          List<MathFunction> result = new ArrayList<>(functions);
61  
62          // Pull up all Products that are directly embedded in this Product
63          for (int index = 0; index < result.size(); index++)
64          {
65              MathFunction function = result.get(index);
66              Throw.whenNull(function, "function");
67              if (function instanceof Product)
68              {
69                  // Replace any embedded Product by all factors that comprise that Product
70                  result.remove(index);
71                  index--;
72                  result.addAll(((Product) function).factors);
73              }
74          }
75          // Optimize all elements
76          for (int index = 0; index < result.size(); index++)
77          {
78              MathFunction function = result.get(index);
79              MathFunction optimized = function.simplify();
80              if (!function.equals(optimized))
81              {
82                  result.remove(index);
83                  result.add(index, optimized);
84              }
85          }
86          Collections.sort(result);
87          // Aggregate all Constant functions and scale factors together and multiply their values
88          // Merge all functions that can be merged 
89          for (int index = 0; index < result.size(); index++)
90          {
91              MathFunction function = result.get(index);
92              if (index < result.size() - 1)
93              {
94                  MathFunction nextFunction = result.get(index + 1);
95                  MathFunction merged = function.mergeMultiply(nextFunction);
96                  if (merged != null)
97                  {
98                      result.remove(index);
99                      result.remove(index);
100                     result.add(index, merged);
101                     index--; // try to merge it with yet one more MathFunction
102                 }
103             }
104         }
105         double productOfConstants = 1.0;
106         for (int index = 0; index < result.size(); index++)
107         {
108             MathFunction function = result.get(index);
109             double scale = function.getScale();
110             if (function instanceof Constant)
111             {
112                 if (0.0 == scale)
113                 {
114                     // This may have to be revised to work in the presence of infinity or NaN values
115                     result.clear();
116                     result.add(Constant.ZERO);
117                     return result;
118                 }
119                 // Remove this Constant and accumulate its value in our running total
120                 productOfConstants *= scale;
121                 result.remove(index);
122                 index--;
123             }
124             else if (scale != 1.0)
125             {
126                 // Accumulate this scale factor in our running total
127                 productOfConstants *= scale;
128                 function = function.scaleBy(1.0 / scale); // remove it from the function and replace the function
129                 result.remove(index);
130                 result.add(index, function);
131             }
132         }
133         if (productOfConstants != 1.0)
134         {
135             if (result.size() > 0)
136             {
137                 // Incorporate the scale factor in the first item of the result
138                 MathFunction function = result.get(0);
139                 result.remove(0);
140                 function = function.scaleBy(productOfConstants);
141                 result.add(0, function);
142             }
143             else // result list is empty
144             {
145                 result.add(new Constant(productOfConstants));
146             }
147         }
148         if (result.size() == 0)
149         {
150             result.add(Constant.ONE);
151         }
152         return result;
153     }
154 
155     @Override
156     public double get(final double x)
157     {
158         double result = 1.0;
159         for (MathFunction fi : this.factors)
160         {
161             result *= fi.get(x);
162         }
163         return result;
164     }
165 
166     @Override
167     public MathFunction getDerivative()
168     {
169         List<MathFunction> result = new ArrayList<>();
170         for (int i = 0; i < this.factors.size(); i++)
171         {
172             List<MathFunction> termFactors = new ArrayList<>(this.factors.size());
173             for (int j = 0; j < this.factors.size(); j++)
174             {
175                 if (j == i)
176                 {
177                     termFactors.add(this.factors.get(j).getDerivative());
178                 }
179                 else
180                 {
181                     termFactors.add(this.factors.get(j));
182                 }
183             }
184             result.add(new Product(termFactors).simplify());
185         }
186         return new Sum(result).simplify();
187     }
188 
189     @Override
190     public MathFunction simplify()
191     {
192         List<MathFunction> simplifiedFactors = simplify(this.factors);
193         if (simplifiedFactors.size() == 1)
194         {
195             return simplifiedFactors.get(0);
196         }
197         return this; // Other simplification were already done in the constructor
198     }
199 
200     @Override
201     public int sortPriority()
202     {
203         return 100;
204     }
205 
206     @Override
207     public int compareWithinSubType(final MathFunction other)
208     {
209         Throw.when(!(other instanceof Product), IllegalArgumentException.class, "other is of wrong type");
210         Product otherProduct = (Product) other;
211         for (int index = 0; index < this.factors.size(); index++)
212         {
213             if (index >= otherProduct.factors.size())
214             {
215                 return 1;
216             }
217             int result = this.factors.get(index).compareTo(otherProduct.factors.get(index));
218             if (result != 0)
219             {
220                 return result;
221             }
222         }
223         if (otherProduct.factors.size() > this.factors.size())
224         {
225             return -1;
226         }
227         return 0;
228     }
229 
230     @Override
231     public MathFunction scaleBy(final double scaleFactor)
232     {
233         if (scaleFactor == 0.0)
234         {
235             return Constant.ZERO;
236         }
237         if (scaleFactor == 1.0)
238         {
239             return this;
240         }
241         List<MathFunction> result = new ArrayList<>(this.factors);
242         MathFunction scaledFactor = result.get(0).scaleBy(scaleFactor);
243         result.remove(0);
244         result.add(0, scaledFactor);
245         return new Product(result);
246     }
247 
248     @Override
249     public KnotReport getKnotReport(final Interval<?> interval)
250     {
251         KnotReport result = KnotReport.NONE;
252         for (MathFunction factor : this.factors)
253         {
254             result = result.combineWith(factor.getKnotReport(interval));
255         }
256         return result;
257     }
258     
259     @Override
260     public SortedSet<Double> getKnots(final Interval<?> interval)
261     {
262         SortedSet<Double> result = new TreeSet<>(); 
263         for (MathFunction factor : this.factors)
264         {
265             result.addAll(factor.getKnots(interval));
266         }
267         return result;
268     }
269 
270     @Override
271     public String toString()
272     {
273         StringBuilder result = new StringBuilder();
274         result.append("\u03A0("); // Capital pi (Π)
275         for (int i = 0; i < this.factors.size(); i++)
276         {
277             if (i > 0)
278             {
279                 result.append(", ");
280             }
281             result.append(this.factors.get(i).toString());
282         }
283         result.append(")");
284         return result.toString();
285     }
286 
287     @Override
288     public int hashCode()
289     {
290         return Objects.hash(this.factors);
291     }
292 
293     @SuppressWarnings("checkstyle:needbraces")
294     @Override
295     public boolean equals(final Object obj)
296     {
297         if (this == obj)
298             return true;
299         if (obj == null)
300             return false;
301         if (getClass() != obj.getClass())
302             return false;
303         Product other = (Product) obj;
304         return Objects.equals(this.factors, other.factors);
305     }
306 
307 }