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
16
17
18
19
20
21
22
23
24
25
26 public class Concatenation implements MathFunction
27 {
28
29 private SortedSet<Interval<MathFunction>> functions;
30
31
32
33
34
35 @SafeVarargs
36 public Concatenation(final Interval<MathFunction>... intervals)
37 {
38 this(convertToSortedSet(intervals));
39 }
40
41
42
43
44
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
59
60
61 public Concatenation(final SortedSet<Interval<MathFunction>> set)
62 {
63
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
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
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
136
137
138
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
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
164
165
166
167
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
191
192
193
194
195
196
197 public Iterator<Interval<Discontinuity>> discontinuities(final Interval<?> interval)
198 {
199 return new Iterator<Interval<Discontinuity>>()
200 {
201
202 private Interval<?> requestedInterval = interval;
203
204
205 private Iterator<Interval<MathFunction>> internalIterator = Concatenation.this.functions.iterator();
206
207
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;
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
331 enum Discontinuity
332 {
333
334 KNOT,
335
336
337 DISCONTINUOUS,
338
339
340 GAP;
341 }
342
343 }