View Javadoc
1   /*
2    * Copyright (C) 2012 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5    * in compliance with the License. You may obtain a copy of the License at
6    *
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software distributed under the License
10   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11   * or implied. See the License for the specific language governing permissions and limitations under
12   * the License.
13   */
14  
15  package com.google.common.math;
16  
17  import static com.google.common.base.Preconditions.checkArgument;
18  import static com.google.common.math.DoubleUtils.isFinite;
19  import static java.lang.Double.NaN;
20  
21  import com.google.common.annotations.Beta;
22  import com.google.common.annotations.GwtIncompatible;
23  import com.google.errorprone.annotations.concurrent.LazyInit;
24  
25  /**
26   * The representation of a linear transformation between real numbers {@code x} and {@code y}.
27   * Graphically, this is the specification of a straight line on a plane. The transformation can be
28   * expressed as {@code y = m * x + c} for finite {@code m} and {@code c}, unless it is a vertical
29   * transformation in which case {@code x} has a constant value for all {@code y}. In the
30   * non-vertical case, {@code m} is the slope of the transformation (and a horizontal transformation
31   * has zero slope).
32   *
33   * @author Pete Gillin
34   * @since 20.0
35   */
36  @Beta
37  @GwtIncompatible
38  public abstract class LinearTransformation {
39  
40    /**
41     * Start building an instance which maps {@code x = x1} to {@code y = y1}. Both arguments must be
42     * finite. Call either {@link LinearTransformationBuilder#and} or
43     * {@link LinearTransformationBuilder#withSlope} on the returned object to finish building the
44     * instance.
45     */
46    public static LinearTransformationBuilder mapping(double x1, double y1) {
47      checkArgument(isFinite(x1) && isFinite(y1));
48      return new LinearTransformationBuilder(x1, y1);
49    }
50  
51    /**
52     * This is an intermediate stage in the construction process. It is returned by
53     * {@link LinearTransformation#mapping}. You almost certainly don't want to keep instances around,
54     * but instead use method chaining. This represents a single point mapping, i.e. a mapping between
55     * one {@code x} and {@code y} value pair.
56     */
57    public static final class LinearTransformationBuilder {
58  
59      private final double x1;
60      private final double y1;
61  
62      private LinearTransformationBuilder(double x1, double y1) {
63        this.x1 = x1;
64        this.y1 = y1;
65      }
66  
67      /**
68       * Finish building an instance which also maps {@code x = x2} to {@code y = y2}. These values
69       * must not both be identical to the values given in the first mapping. If only the {@code x}
70       * values are identical, the transformation is vertical. If only the {@code y} values are
71       * identical, the transformation is horizontal (i.e. the slope is zero).
72       */
73      public LinearTransformation and(double x2, double y2) {
74        checkArgument(isFinite(x2) && isFinite(y2));
75        if (x2 == x1) {
76          checkArgument(y2 != y1);
77          return new VerticalLinearTransformation(x1);
78        } else {
79          return withSlope((y2 - y1) / (x2 - x1));
80        }
81      }
82  
83      /**
84       * Finish building an instance with the given slope, i.e. the rate of change of {@code y} with
85       * respect to {@code x}. The slope must not be {@code NaN}. It may be infinite, in which case
86       * the transformation is vertical. (If it is zero, the transformation is horizontal.)
87       */
88      public LinearTransformation withSlope(double slope) {
89        checkArgument(!Double.isNaN(slope));
90        if (isFinite(slope)) {
91          double yIntercept = y1 - x1 * slope;
92          return new RegularLinearTransformation(slope, yIntercept);
93        } else {
94          return new VerticalLinearTransformation(x1);
95        }
96      }
97    }
98  
99    /**
100    * Builds an instance representing a vertical transformation with a constant value of {@code x}.
101    * (The inverse of this will be a horizontal transformation.)
102    */
103   public static LinearTransformation vertical(double x) {
104     checkArgument(isFinite(x));
105     return new VerticalLinearTransformation(x);
106   }
107 
108   /**
109    * Builds an instance representing a horizontal transformation with a constant value of {@code y}.
110    * (The inverse of this will be a vertical transformation.)
111    */
112   public static LinearTransformation horizontal(double y) {
113     checkArgument(isFinite(y));
114     double slope = 0.0;
115     return new RegularLinearTransformation(slope, y);
116   }
117 
118   /**
119    * Builds an instance for datasets which contains {@link Double#NaN}. The {@link #isHorizontal}
120    * and {@link #isVertical} methods return {@code false} and the {@link #slope}, and
121    * {@link #transform} methods all return {@link Double#NaN}. The {@link #inverse} method returns
122    * the same instance.
123    */
124   public static LinearTransformation forNaN() {
125     return NaNLinearTransformation.INSTANCE;
126   }
127 
128   /**
129    * Returns whether this is a vertical transformation.
130    */
131   public abstract boolean isVertical();
132 
133   /**
134    * Returns whether this is a horizontal transformation.
135    */
136   public abstract boolean isHorizontal();
137 
138   /**
139    * Returns the slope of the transformation, i.e. the rate of change of {@code y} with respect to
140    * {@code x}. This must not be called on a vertical transformation (i.e. when
141    * {@link #isVertical()} is true).
142    */
143   public abstract double slope();
144 
145   /**
146    * Returns the {@code y} corresponding to the given {@code x}. This must not be called on a
147    * vertical transformation (i.e. when {@link #isVertical()} is true).
148    */
149   public abstract double transform(double x);
150 
151   /**
152    * Returns the inverse linear transformation. The inverse of a horizontal transformation is a
153    * vertical transformation, and vice versa. The inverse of the {@link #forNaN} transformation is
154    * itself. In all other cases, the inverse is a transformation such that applying both the
155    * original transformation and its inverse to a value gives you the original value give-or-take
156    * numerical errors. Calling this method multiple times on the same instance will always return
157    * the same instance. Calling this method on the result of calling this method on an instance will
158    * always return that original instance.
159    */
160   public abstract LinearTransformation inverse();
161 
162   private static final class RegularLinearTransformation extends LinearTransformation {
163 
164     final double slope;
165     final double yIntercept;
166 
167     @LazyInit
168     LinearTransformation inverse;
169 
170     RegularLinearTransformation(double slope, double yIntercept) {
171       this.slope = slope;
172       this.yIntercept = yIntercept;
173       this.inverse = null; // to be lazily initialized
174     }
175 
176     RegularLinearTransformation(double slope, double yIntercept, LinearTransformation inverse) {
177       this.slope = slope;
178       this.yIntercept = yIntercept;
179       this.inverse = inverse;
180     }
181 
182     @Override
183     public boolean isVertical() {
184       return false;
185     }
186 
187     @Override
188     public boolean isHorizontal() {
189       return (slope == 0.0);
190     }
191 
192     @Override
193     public double slope() {
194       return slope;
195     }
196 
197     @Override
198     public double transform(double x) {
199       return x * slope + yIntercept;
200     }
201 
202     @Override
203     public LinearTransformation inverse() {
204       LinearTransformation result = inverse;
205       return (result == null) ? inverse = createInverse() : result;
206     }
207 
208     @Override
209     public String toString() {
210       return String.format("y = %g * x + %g", slope, yIntercept);
211     }
212 
213     private LinearTransformation createInverse() {
214       if (slope != 0.0) {
215         return new RegularLinearTransformation(1.0 / slope, -1.0 * yIntercept / slope, this);
216       } else {
217         return new VerticalLinearTransformation(yIntercept, this);
218       }
219     }
220   }
221 
222   private static final class VerticalLinearTransformation extends LinearTransformation {
223 
224     final double x;
225 
226     @LazyInit
227     LinearTransformation inverse;
228 
229     VerticalLinearTransformation(double x) {
230       this.x = x;
231       this.inverse = null; // to be lazily initialized
232     }
233 
234     VerticalLinearTransformation(double x, LinearTransformation inverse) {
235       this.x = x;
236       this.inverse = inverse;
237     }
238 
239     @Override
240     public boolean isVertical() {
241       return true;
242     }
243 
244     @Override
245     public boolean isHorizontal() {
246       return false;
247     }
248 
249     @Override
250     public double slope() {
251       throw new IllegalStateException();
252     }
253 
254     @Override
255     public double transform(double x) {
256       throw new IllegalStateException();
257     }
258 
259     @Override
260     public LinearTransformation inverse() {
261       LinearTransformation result = inverse;
262       return (result == null) ? inverse = createInverse() : result;
263     }
264 
265     @Override
266     public String toString() {
267       return String.format("x = %g", x);
268     }
269 
270     private LinearTransformation createInverse() {
271       return new RegularLinearTransformation(0.0, x, this);
272     }
273   }
274 
275   private static final class NaNLinearTransformation extends LinearTransformation {
276 
277     static final NaNLinearTransformation INSTANCE = new NaNLinearTransformation();
278 
279     @Override
280     public boolean isVertical() {
281       return false;
282     }
283 
284     @Override
285     public boolean isHorizontal() {
286       return false;
287     }
288 
289     @Override
290     public double slope() {
291       return NaN;
292     }
293 
294     @Override
295     public double transform(double x) {
296       return NaN;
297     }
298 
299     @Override
300     public LinearTransformation inverse() {
301       return this;
302     }
303 
304     @Override
305     public String toString() {
306       return "NaN";
307     }
308   }
309 }