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