001/* 002 * Copyright (C) 2012 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the License 010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 011 * or implied. See the License for the specific language governing permissions and limitations under 012 * the License. 013 */ 014 015package com.google.common.math; 016 017import static com.google.common.base.Preconditions.checkArgument; 018import static com.google.common.math.DoubleUtils.isFinite; 019import static java.lang.Double.NaN; 020 021import com.google.common.annotations.Beta; 022import com.google.common.annotations.GwtIncompatible; 023import com.google.errorprone.annotations.concurrent.LazyInit; 024 025/** 026 * The representation of a linear transformation between real numbers {@code x} and {@code y}. 027 * Graphically, this is the specification of a straight line on a plane. The transformation can be 028 * expressed as {@code y = m * x + c} for finite {@code m} and {@code c}, unless it is a vertical 029 * transformation in which case {@code x} has a constant value for all {@code y}. In the 030 * non-vertical case, {@code m} is the slope of the transformation (and a horizontal transformation 031 * has zero slope). 032 * 033 * @author Pete Gillin 034 * @since 20.0 035 */ 036@Beta 037@GwtIncompatible 038public abstract class LinearTransformation { 039 040 /** 041 * Start building an instance which maps {@code x = x1} to {@code y = y1}. Both arguments must be 042 * finite. Call either {@link LinearTransformationBuilder#and} or 043 * {@link LinearTransformationBuilder#withSlope} on the returned object to finish building the 044 * instance. 045 */ 046 public static LinearTransformationBuilder mapping(double x1, double y1) { 047 checkArgument(isFinite(x1) && isFinite(y1)); 048 return new LinearTransformationBuilder(x1, y1); 049 } 050 051 /** 052 * This is an intermediate stage in the construction process. It is returned by 053 * {@link LinearTransformation#mapping}. You almost certainly don't want to keep instances around, 054 * but instead use method chaining. This represents a single point mapping, i.e. a mapping between 055 * one {@code x} and {@code y} value pair. 056 * 057 * @since 20.0 058 */ 059 public static final class LinearTransformationBuilder { 060 061 private final double x1; 062 private final double y1; 063 064 private LinearTransformationBuilder(double x1, double y1) { 065 this.x1 = x1; 066 this.y1 = y1; 067 } 068 069 /** 070 * Finish building an instance which also maps {@code x = x2} to {@code y = y2}. These values 071 * must not both be identical to the values given in the first mapping. If only the {@code x} 072 * values are identical, the transformation is vertical. If only the {@code y} values are 073 * identical, the transformation is horizontal (i.e. the slope is zero). 074 */ 075 public LinearTransformation and(double x2, double y2) { 076 checkArgument(isFinite(x2) && isFinite(y2)); 077 if (x2 == x1) { 078 checkArgument(y2 != y1); 079 return new VerticalLinearTransformation(x1); 080 } else { 081 return withSlope((y2 - y1) / (x2 - x1)); 082 } 083 } 084 085 /** 086 * Finish building an instance with the given slope, i.e. the rate of change of {@code y} with 087 * respect to {@code x}. The slope must not be {@code NaN}. It may be infinite, in which case 088 * the transformation is vertical. (If it is zero, the transformation is horizontal.) 089 */ 090 public LinearTransformation withSlope(double slope) { 091 checkArgument(!Double.isNaN(slope)); 092 if (isFinite(slope)) { 093 double yIntercept = y1 - x1 * slope; 094 return new RegularLinearTransformation(slope, yIntercept); 095 } else { 096 return new VerticalLinearTransformation(x1); 097 } 098 } 099 } 100 101 /** 102 * Builds an instance representing a vertical transformation with a constant value of {@code x}. 103 * (The inverse of this will be a horizontal transformation.) 104 */ 105 public static LinearTransformation vertical(double x) { 106 checkArgument(isFinite(x)); 107 return new VerticalLinearTransformation(x); 108 } 109 110 /** 111 * Builds an instance representing a horizontal transformation with a constant value of {@code y}. 112 * (The inverse of this will be a vertical transformation.) 113 */ 114 public static LinearTransformation horizontal(double y) { 115 checkArgument(isFinite(y)); 116 double slope = 0.0; 117 return new RegularLinearTransformation(slope, y); 118 } 119 120 /** 121 * Builds an instance for datasets which contains {@link Double#NaN}. The {@link #isHorizontal} 122 * and {@link #isVertical} methods return {@code false} and the {@link #slope}, and 123 * {@link #transform} methods all return {@link Double#NaN}. The {@link #inverse} method returns 124 * the same instance. 125 */ 126 public static LinearTransformation forNaN() { 127 return NaNLinearTransformation.INSTANCE; 128 } 129 130 /** 131 * Returns whether this is a vertical transformation. 132 */ 133 public abstract boolean isVertical(); 134 135 /** 136 * Returns whether this is a horizontal transformation. 137 */ 138 public abstract boolean isHorizontal(); 139 140 /** 141 * Returns the slope of the transformation, i.e. the rate of change of {@code y} with respect to 142 * {@code x}. This must not be called on a vertical transformation (i.e. when 143 * {@link #isVertical()} is true). 144 */ 145 public abstract double slope(); 146 147 /** 148 * Returns the {@code y} corresponding to the given {@code x}. This must not be called on a 149 * vertical transformation (i.e. when {@link #isVertical()} is true). 150 */ 151 public abstract double transform(double x); 152 153 /** 154 * Returns the inverse linear transformation. The inverse of a horizontal transformation is a 155 * vertical transformation, and vice versa. The inverse of the {@link #forNaN} transformation is 156 * itself. In all other cases, the inverse is a transformation such that applying both the 157 * original transformation and its inverse to a value gives you the original value give-or-take 158 * numerical errors. Calling this method multiple times on the same instance will always return 159 * the same instance. Calling this method on the result of calling this method on an instance will 160 * always return that original instance. 161 */ 162 public abstract LinearTransformation inverse(); 163 164 private static final class RegularLinearTransformation extends LinearTransformation { 165 166 final double slope; 167 final double yIntercept; 168 169 @LazyInit 170 LinearTransformation inverse; 171 172 RegularLinearTransformation(double slope, double yIntercept) { 173 this.slope = slope; 174 this.yIntercept = yIntercept; 175 this.inverse = null; // to be lazily initialized 176 } 177 178 RegularLinearTransformation(double slope, double yIntercept, LinearTransformation inverse) { 179 this.slope = slope; 180 this.yIntercept = yIntercept; 181 this.inverse = inverse; 182 } 183 184 @Override 185 public boolean isVertical() { 186 return false; 187 } 188 189 @Override 190 public boolean isHorizontal() { 191 return (slope == 0.0); 192 } 193 194 @Override 195 public double slope() { 196 return slope; 197 } 198 199 @Override 200 public double transform(double x) { 201 return x * slope + yIntercept; 202 } 203 204 @Override 205 public LinearTransformation inverse() { 206 LinearTransformation result = inverse; 207 return (result == null) ? inverse = createInverse() : result; 208 } 209 210 @Override 211 public String toString() { 212 return String.format("y = %g * x + %g", slope, yIntercept); 213 } 214 215 private LinearTransformation createInverse() { 216 if (slope != 0.0) { 217 return new RegularLinearTransformation(1.0 / slope, -1.0 * yIntercept / slope, this); 218 } else { 219 return new VerticalLinearTransformation(yIntercept, this); 220 } 221 } 222 } 223 224 private static final class VerticalLinearTransformation extends LinearTransformation { 225 226 final double x; 227 228 @LazyInit 229 LinearTransformation inverse; 230 231 VerticalLinearTransformation(double x) { 232 this.x = x; 233 this.inverse = null; // to be lazily initialized 234 } 235 236 VerticalLinearTransformation(double x, LinearTransformation inverse) { 237 this.x = x; 238 this.inverse = inverse; 239 } 240 241 @Override 242 public boolean isVertical() { 243 return true; 244 } 245 246 @Override 247 public boolean isHorizontal() { 248 return false; 249 } 250 251 @Override 252 public double slope() { 253 throw new IllegalStateException(); 254 } 255 256 @Override 257 public double transform(double x) { 258 throw new IllegalStateException(); 259 } 260 261 @Override 262 public LinearTransformation inverse() { 263 LinearTransformation result = inverse; 264 return (result == null) ? inverse = createInverse() : result; 265 } 266 267 @Override 268 public String toString() { 269 return String.format("x = %g", x); 270 } 271 272 private LinearTransformation createInverse() { 273 return new RegularLinearTransformation(0.0, x, this); 274 } 275 } 276 277 private static final class NaNLinearTransformation extends LinearTransformation { 278 279 static final NaNLinearTransformation INSTANCE = new NaNLinearTransformation(); 280 281 @Override 282 public boolean isVertical() { 283 return false; 284 } 285 286 @Override 287 public boolean isHorizontal() { 288 return false; 289 } 290 291 @Override 292 public double slope() { 293 return NaN; 294 } 295 296 @Override 297 public double transform(double x) { 298 return NaN; 299 } 300 301 @Override 302 public LinearTransformation inverse() { 303 return this; 304 } 305 306 @Override 307 public String toString() { 308 return "NaN"; 309 } 310 } 311}