aboutsummaryrefslogtreecommitdiff
path: root/guava/src/com/google/common/math/LinearTransformation.java
blob: 485b04660902e03e571ce352d8c6208890ccf967 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/*
 * Copyright (C) 2012 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.common.math;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.math.DoubleUtils.isFinite;
import static java.lang.Double.NaN;

import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtIncompatible;
import com.google.errorprone.annotations.concurrent.LazyInit;

/**
 * The representation of a linear transformation between real numbers {@code x} and {@code y}.
 * Graphically, this is the specification of a straight line on a plane. The transformation can be
 * expressed as {@code y = m * x + c} for finite {@code m} and {@code c}, unless it is a vertical
 * transformation in which case {@code x} has a constant value for all {@code y}. In the
 * non-vertical case, {@code m} is the slope of the transformation (and a horizontal transformation
 * has zero slope).
 *
 * @author Pete Gillin
 * @since 20.0
 */
@Beta
@GwtIncompatible
public abstract class LinearTransformation {

  /**
   * Start building an instance which maps {@code x = x1} to {@code y = y1}. Both arguments must be
   * finite. Call either {@link LinearTransformationBuilder#and} or {@link
   * LinearTransformationBuilder#withSlope} on the returned object to finish building the instance.
   */
  public static LinearTransformationBuilder mapping(double x1, double y1) {
    checkArgument(isFinite(x1) && isFinite(y1));
    return new LinearTransformationBuilder(x1, y1);
  }

  /**
   * This is an intermediate stage in the construction process. It is returned by {@link
   * LinearTransformation#mapping}. You almost certainly don't want to keep instances around, but
   * instead use method chaining. This represents a single point mapping, i.e. a mapping between one
   * {@code x} and {@code y} value pair.
   *
   * @since 20.0
   */
  public static final class LinearTransformationBuilder {

    private final double x1;
    private final double y1;

    private LinearTransformationBuilder(double x1, double y1) {
      this.x1 = x1;
      this.y1 = y1;
    }

    /**
     * Finish building an instance which also maps {@code x = x2} to {@code y = y2}. These values
     * must not both be identical to the values given in the first mapping. If only the {@code x}
     * values are identical, the transformation is vertical. If only the {@code y} values are
     * identical, the transformation is horizontal (i.e. the slope is zero).
     */
    public LinearTransformation and(double x2, double y2) {
      checkArgument(isFinite(x2) && isFinite(y2));
      if (x2 == x1) {
        checkArgument(y2 != y1);
        return new VerticalLinearTransformation(x1);
      } else {
        return withSlope((y2 - y1) / (x2 - x1));
      }
    }

    /**
     * Finish building an instance with the given slope, i.e. the rate of change of {@code y} with
     * respect to {@code x}. The slope must not be {@code NaN}. It may be infinite, in which case
     * the transformation is vertical. (If it is zero, the transformation is horizontal.)
     */
    public LinearTransformation withSlope(double slope) {
      checkArgument(!Double.isNaN(slope));
      if (isFinite(slope)) {
        double yIntercept = y1 - x1 * slope;
        return new RegularLinearTransformation(slope, yIntercept);
      } else {
        return new VerticalLinearTransformation(x1);
      }
    }
  }

  /**
   * Builds an instance representing a vertical transformation with a constant value of {@code x}.
   * (The inverse of this will be a horizontal transformation.)
   */
  public static LinearTransformation vertical(double x) {
    checkArgument(isFinite(x));
    return new VerticalLinearTransformation(x);
  }

  /**
   * Builds an instance representing a horizontal transformation with a constant value of {@code y}.
   * (The inverse of this will be a vertical transformation.)
   */
  public static LinearTransformation horizontal(double y) {
    checkArgument(isFinite(y));
    double slope = 0.0;
    return new RegularLinearTransformation(slope, y);
  }

  /**
   * Builds an instance for datasets which contains {@link Double#NaN}. The {@link #isHorizontal}
   * and {@link #isVertical} methods return {@code false} and the {@link #slope}, and {@link
   * #transform} methods all return {@link Double#NaN}. The {@link #inverse} method returns the same
   * instance.
   */
  public static LinearTransformation forNaN() {
    return NaNLinearTransformation.INSTANCE;
  }

  /** Returns whether this is a vertical transformation. */
  public abstract boolean isVertical();

  /** Returns whether this is a horizontal transformation. */
  public abstract boolean isHorizontal();

  /**
   * Returns the slope of the transformation, i.e. the rate of change of {@code y} with respect to
   * {@code x}. This must not be called on a vertical transformation (i.e. when {@link
   * #isVertical()} is true).
   */
  public abstract double slope();

  /**
   * Returns the {@code y} corresponding to the given {@code x}. This must not be called on a
   * vertical transformation (i.e. when {@link #isVertical()} is true).
   */
  public abstract double transform(double x);

  /**
   * Returns the inverse linear transformation. The inverse of a horizontal transformation is a
   * vertical transformation, and vice versa. The inverse of the {@link #forNaN} transformation is
   * itself. In all other cases, the inverse is a transformation such that applying both the
   * original transformation and its inverse to a value gives you the original value give-or-take
   * numerical errors. Calling this method multiple times on the same instance will always return
   * the same instance. Calling this method on the result of calling this method on an instance will
   * always return that original instance.
   */
  public abstract LinearTransformation inverse();

  private static final class RegularLinearTransformation extends LinearTransformation {

    final double slope;
    final double yIntercept;

    @LazyInit LinearTransformation inverse;

    RegularLinearTransformation(double slope, double yIntercept) {
      this.slope = slope;
      this.yIntercept = yIntercept;
      this.inverse = null; // to be lazily initialized
    }

    RegularLinearTransformation(double slope, double yIntercept, LinearTransformation inverse) {
      this.slope = slope;
      this.yIntercept = yIntercept;
      this.inverse = inverse;
    }

    @Override
    public boolean isVertical() {
      return false;
    }

    @Override
    public boolean isHorizontal() {
      return (slope == 0.0);
    }

    @Override
    public double slope() {
      return slope;
    }

    @Override
    public double transform(double x) {
      return x * slope + yIntercept;
    }

    @Override
    public LinearTransformation inverse() {
      LinearTransformation result = inverse;
      return (result == null) ? inverse = createInverse() : result;
    }

    @Override
    public String toString() {
      return String.format("y = %g * x + %g", slope, yIntercept);
    }

    private LinearTransformation createInverse() {
      if (slope != 0.0) {
        return new RegularLinearTransformation(1.0 / slope, -1.0 * yIntercept / slope, this);
      } else {
        return new VerticalLinearTransformation(yIntercept, this);
      }
    }
  }

  private static final class VerticalLinearTransformation extends LinearTransformation {

    final double x;

    @LazyInit LinearTransformation inverse;

    VerticalLinearTransformation(double x) {
      this.x = x;
      this.inverse = null; // to be lazily initialized
    }

    VerticalLinearTransformation(double x, LinearTransformation inverse) {
      this.x = x;
      this.inverse = inverse;
    }

    @Override
    public boolean isVertical() {
      return true;
    }

    @Override
    public boolean isHorizontal() {
      return false;
    }

    @Override
    public double slope() {
      throw new IllegalStateException();
    }

    @Override
    public double transform(double x) {
      throw new IllegalStateException();
    }

    @Override
    public LinearTransformation inverse() {
      LinearTransformation result = inverse;
      return (result == null) ? inverse = createInverse() : result;
    }

    @Override
    public String toString() {
      return String.format("x = %g", x);
    }

    private LinearTransformation createInverse() {
      return new RegularLinearTransformation(0.0, x, this);
    }
  }

  private static final class NaNLinearTransformation extends LinearTransformation {

    static final NaNLinearTransformation INSTANCE = new NaNLinearTransformation();

    @Override
    public boolean isVertical() {
      return false;
    }

    @Override
    public boolean isHorizontal() {
      return false;
    }

    @Override
    public double slope() {
      return NaN;
    }

    @Override
    public double transform(double x) {
      return NaN;
    }

    @Override
    public LinearTransformation inverse() {
      return this;
    }

    @Override
    public String toString() {
      return "NaN";
    }
  }
}