View Javadoc
1   package org.djutils.math.functions;
2   
3   import java.util.Iterator;
4   import java.util.Map.Entry;
5   import java.util.NoSuchElementException;
6   import java.util.Objects;
7   import java.util.SortedMap;
8   import java.util.SortedSet;
9   import java.util.TreeMap;
10  import java.util.TreeSet;
11  
12  import org.djutils.exceptions.Throw;
13  
14  /**
15   * Concatenate FunctionInterface objects
16   * <p>
17   * Copyright (c) 2024-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
18   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
19   * distributed under a three-clause BSD-style license, which can be found at
20   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
21   * </p>
22   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
23   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
24   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
25   */
26  public class Concatenation implements MathFunction
27  {
28      /** The wrapped functions. */
29      private SortedSet<Interval<MathFunction>> functions;
30  
31      /**
32       * Construct the concatenation of one or more MathFunction objects.
33       * @param intervals the functions and the domains over which they should be active
34       */
35      @SafeVarargs
36      public Concatenation(final Interval<MathFunction>... intervals)
37      {
38          this(convertToSortedSet(intervals));
39      }
40  
41      /**
42       * Convert an array of Interval&lt;FunctionInterface&gt; to a SortedSet.
43       * @param intervals the intervals
44       * @return sorted set
45       */
46      @SafeVarargs
47      private static SortedSet<Interval<MathFunction>> convertToSortedSet(final Interval<MathFunction>... intervals)
48      {
49          SortedSet<Interval<MathFunction>> result = new TreeSet<>();
50          for (var interval : intervals)
51          {
52              result.add(interval);
53          }
54          return result;
55      }
56  
57      /**
58       * Construct a Concatenation from a sorted set of Interval&lt;MathFunction&gt;.
59       * @param set the sorted set of Interval with MathFunction payload
60       */
61      public Concatenation(final SortedSet<Interval<MathFunction>> set)
62      {
63          // Run the ordered list and check for overlaps and add NaN functions where there are gaps
64          Interval<MathFunction> prevInterval = null;
65          for (var interval : set)
66          {
67              Interval<MathFunction> thisInterval = interval;
68              if (prevInterval != null)
69              {
70                  Throw.when(!prevInterval.disjunct(thisInterval), IllegalArgumentException.class,
71                          "Overlapping domains not permitted");
72                  if (prevInterval.high() < thisInterval.low() || (prevInterval.high() == thisInterval.low()
73                          && (!prevInterval.highInclusive()) && (!thisInterval.lowInclusive())))
74                  {
75                      // There is a gap; fill it with a NaN function
76                      set.add(new Interval<MathFunction>(prevInterval.high(), !prevInterval.highInclusive(), thisInterval.low(),
77                              !thisInterval.lowInclusive(), Nan.NAN));
78                  }
79              }
80              prevInterval = thisInterval;
81          }
82          Throw.when(set.size() < 1, IllegalArgumentException.class, "need at least one argument");
83          this.functions = set;
84      }
85  
86      @Override
87      public Double apply(final Double x)
88      {
89          // TODO Use bisection to home in on the interval that covers x; for now use linear search
90          for (var interval : this.functions)
91          {
92              if (interval.covers(x))
93              {
94                  return interval.payload().apply(x);
95              }
96          }
97          throw new IllegalArgumentException(String.format("x is outside the combined domain of this Concatenation", x));
98      }
99  
100     @Override
101     public Concatenation getDerivative()
102     {
103         SortedSet<Interval<MathFunction>> set = new TreeSet<>();
104         for (var interval : this.functions)
105         {
106             set.add(new Interval<MathFunction>(interval.low(), interval.lowInclusive(), interval.high(),
107                     interval.highInclusive(), interval.payload().getDerivative()));
108         }
109         return new Concatenation(set);
110     }
111 
112     @Override
113     public MathFunction scaleBy(final double factor)
114     {
115         if (factor == 1.0)
116         {
117             return this;
118         }
119         SortedSet<Interval<MathFunction>> result = new TreeSet<>();
120         for (Interval<MathFunction> interval : this.functions)
121         {
122             result.add(new Interval<MathFunction>(interval.low(), interval.lowInclusive(), interval.high(),
123                     interval.highInclusive(), interval.payload().scaleBy(factor)));
124         }
125         return new Concatenation(result);
126     }
127 
128     @Override
129     public int sortPriority()
130     {
131         return 110;
132     }
133 
134     /**
135      * Construct a concatenation that is piecewise linear through a given set of points.
136      * @param map mapping from domain to value at the inflection points
137      * @return new Concatenation that is piecewise linear and connects the given points
138      * @throws IllegalArgumentException when <code>map</code> contains fewer than 2 entries
139      */
140     public static Concatenation continuousPiecewiseLinear(final SortedMap<Double, Double> map)
141     {
142         SortedSet<Interval<MathFunction>> intervals = new TreeSet<>();
143         Entry<Double, Double> prevEntry = null;
144         for (Entry<Double, Double> nextEntry : map.entrySet())
145         {
146             if (prevEntry != null)
147             {
148                 // create one linear section
149                 double slope = (nextEntry.getValue() - prevEntry.getValue()) / (nextEntry.getKey() - prevEntry.getKey());
150                 Power powerFunction = new Power(slope, 1);
151                 double constant = prevEntry.getValue() - powerFunction.apply(prevEntry.getKey());
152                 MathFunction function = new Sum(new Constant(constant), powerFunction);
153                 intervals.add(new Interval<MathFunction>(prevEntry.getKey(), intervals.isEmpty(), nextEntry.getKey(), true,
154                         function));
155             }
156             prevEntry = nextEntry;
157         }
158         Throw.when(intervals.isEmpty(), IllegalArgumentException.class, "need at least two points");
159         return new Concatenation(intervals);
160     }
161 
162     /**
163      * Construct a concatenation that is piecewise linear through a given set of input-output pairs.
164      * @param arguments the input-output pairs; these specify the inflection points
165      * @return new Concatenation that is piecewise linear and connects the given points
166      * @throws IllegalArgumentException when <code>arguments</code> contains an odd number of entries, or fewer than 2 domain
167      *             values, or duplicate domain values with differing function values
168      */
169     public static Concatenation continuousPiecewiseLinear(final double... arguments)
170     {
171         Throw.when(arguments.length % 2 != 0, IllegalArgumentException.class, "need an even number of arguments");
172         SortedMap<Double, Double> map = new TreeMap<>();
173         for (int i = 0; i < arguments.length; i += 2)
174         {
175             Throw.when(map.containsKey(arguments[i]) && arguments[i + 1] != map.get(arguments[i]),
176                     IllegalArgumentException.class, "duplicate domain value with different function value is not permitted");
177             map.put(arguments[i], arguments[i + 1]);
178         }
179         return continuousPiecewiseLinear(map);
180     }
181 
182     @Override
183     public int compareWithinSubType(final MathFunction other)
184     {
185         Throw.when(!(other instanceof Concatenation), IllegalArgumentException.class, "other is of wrong type");
186         return 0;
187     }
188 
189     /**
190      * Report all non-continuities and all points where <code>this</code> function is non differentiable, or non-evaluable. If
191      * another <code>MathFunction</code> is chained, the transformation of that function, nor any discontinuities of that
192      * <code>MathFunction</code> are taken into account as there is (currently) no way to figure out what values of the domain
193      * of the chained function result in values that correspond to the discontinuities of <code>this</code> function.
194      * @param interval the interval on which to report the discontinuities
195      * @return iterator that will generate all discontinuities in the interval
196      */
197     public Iterator<Interval<Discontinuity>> discontinuities(final Interval<?> interval)
198     {
199         return new Iterator<Interval<Discontinuity>>()
200         {
201             /** The interval over which the discontinuities were requested. */
202             private Interval<?> requestedInterval = interval;
203 
204             /** Iterator that visits all the internal intervals/functions of the Concatenation in sequence. */
205             private Iterator<Interval<MathFunction>> internalIterator = Concatenation.this.functions.iterator();
206 
207             /** The current interval (made available by the hasNext method and cleared by the next method). */
208             private Interval<MathFunction> currentInterval = null;
209 
210             @Override
211             public boolean hasNext()
212             {
213                 if (this.currentInterval == null && (!this.internalIterator.hasNext()))
214                 {
215                     return false; // out of data
216                 }
217                 while (this.currentInterval == null && this.internalIterator.hasNext())
218                 {
219                     this.currentInterval = this.internalIterator.next().intersection(this.requestedInterval);
220                 }
221                 return this.currentInterval != null;
222             }
223 
224             @Override
225             public Interval<Discontinuity> next()
226             {
227                 Throw.when(this.currentInterval == null, NoSuchElementException.class, "Out of data");
228                 Interval<Discontinuity> result = this.currentInterval.payload() instanceof Nan
229                         ? new Interval<>(this.currentInterval.low(), true, this.currentInterval.high(),
230                                 this.currentInterval.highInclusive(), Discontinuity.GAP)
231                         : new Interval<>(this.currentInterval.low(), true, this.currentInterval.low(), true,
232                                 Discontinuity.KNOT);
233                 this.currentInterval = null;
234                 return result;
235             }
236         };
237     }
238 
239     @Override
240     public KnotReport getKnotReport(final Interval<?> interval)
241     {
242         KnotReport result = KnotReport.NONE;
243         if (this.functions.first().low() > interval.low() || (this.functions.last().high() < interval.high()))
244         {
245             result = KnotReport.KNOWN_INFINITE;
246         }
247         for (Interval<MathFunction> i : this.functions)
248         {
249             Interval<MathFunction> intersection = i.intersection(interval);
250             if (intersection != null)
251             {
252                 if (interval.covers(i.low()))
253                 {
254                     result = result.combineWith(KnotReport.KNOWN_FINITE);
255                 }
256                 if (interval.covers(i.high()))
257                 {
258                     result = result.combineWith(KnotReport.KNOWN_FINITE);
259                 }
260                 result = result.combineWith(i.payload().getKnotReport(interval));
261             }
262         }
263         return result;
264     }
265 
266     @Override
267     public SortedSet<Double> getKnots(final Interval<?> interval)
268     {
269         Throw.when(this.functions.first().low() > interval.low() || (this.functions.last().high() < interval.high()),
270                 UnsupportedOperationException.class, "Concatentation is undefined over (part of) " + interval);
271         SortedSet<Double> result = new TreeSet<Double>();
272         for (Interval<MathFunction> i : this.functions)
273         {
274             Interval<MathFunction> intersection = i.intersection(interval);
275             if (intersection != null)
276             {
277                 result.addAll(i.payload().getKnots(interval));
278             }
279             if (interval.covers(i.low()))
280             {
281                 result.add(i.low());
282             }
283             if (interval.covers(i.high()))
284             {
285                 result.add(i.high());
286             }
287         }
288         return result;
289     }
290 
291     @Override
292     public String toString()
293     {
294         StringBuilder stringBuilder = new StringBuilder();
295         stringBuilder.append("IntervalSet(");
296         boolean first = true;
297         for (var interval : this.functions)
298         {
299             if (!first)
300             {
301                 stringBuilder.append(", ");
302             }
303             stringBuilder.append(interval.toString());
304             first = false;
305         }
306         stringBuilder.append(")");
307         return stringBuilder.toString();
308     }
309 
310     @Override
311     public int hashCode()
312     {
313         return Objects.hash(this.functions);
314     }
315 
316     @SuppressWarnings("checkstyle:needbraces")
317     @Override
318     public boolean equals(final Object obj)
319     {
320         if (this == obj)
321             return true;
322         if (obj == null)
323             return false;
324         if (getClass() != obj.getClass())
325             return false;
326         Concatenation other = (Concatenation) obj;
327         return Objects.equals(this.functions, other.functions);
328     }
329 
330     /** The various discontinuities reported by the <code>discontinuities</code> method. */
331     enum Discontinuity
332     {
333         /** Continuous, but not differentiable. */
334         KNOT,
335 
336         /** Not continuous (and, therefore, not differentiable). */
337         DISCONTINUOUS,
338 
339         /** Function undefined in this interval; the <code>MathFunction</code> will yield <code>NaN</code> in this interval. */
340         GAP;
341     }
342 
343 }