Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

d3-style piecewise scales #728

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.onthegomap.planetiler.benchmarks;

import com.onthegomap.planetiler.util.Scales;
import java.util.function.DoubleUnaryOperator;
import java.util.function.Supplier;

public class BenchmarkInterpolator {
private static double sum = 0;

public static void main(String[] args) {
long times = 10_000_000;
benchmarkInterpolator("linear", times, Scales::linear);
benchmarkInterpolator("sqrt", times, Scales::sqrt);
benchmarkInterpolator("pow2", times, () -> Scales.power(2));
benchmarkInterpolator("pow10", times, () -> Scales.power(1));
benchmarkInterpolator("log", times, () -> Scales.log(10));
benchmarkInterpolator("log2", times, () -> Scales.log(2));
System.err.println(sum);
}

private static void benchmarkInterpolator(String name, long times, Supplier<Scales.DoubleContinuous> get) {
benchmarkAndInverted(name + "_2", 1, 2, times, () -> get.get().put(1, 1d).put(2, 2d));
benchmarkAndInverted(name + "_3", 1, 2, times, () -> get.get().put(1, 1d).put(1.5, 2d).put(2, 3d));
}

private static void benchmarkAndInverted(String name, double start, double end, long steps,
Supplier<Scales.DoubleContinuous> build) {
benchmark(name + "_f", start, end, steps, build::get);
benchmark(name + "_i", start, end, steps, () -> build.get().invert());
}

private static void benchmark(String name, double start, double end, long steps,
Supplier<DoubleUnaryOperator> build) {
double delta = (end - start) / steps;
double x = start;
double result = 0;
for (long i = 0; i < steps / 10_000; i++) {
result += build.get().applyAsDouble(x += delta);
}
x = start;
long a = System.currentTimeMillis();
for (long i = 0; i < steps; i++) {
result += build.get().applyAsDouble(x += delta);
}
x = start;
var preBuilt = build.get();
long b = System.currentTimeMillis();
for (long i = 0; i < steps; i++) {
result += preBuilt.applyAsDouble(x += delta);
}
long c = System.currentTimeMillis();
sum += result;
System.err.println(name + "\t" + (b - a) + "\t" + (c - b));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.onthegomap.planetiler.util;

import java.util.function.BiFunction;
import java.util.function.DoubleFunction;
import java.util.function.DoubleUnaryOperator;

public interface IInterpolator<T extends IInterpolator<T, V>, V> extends DoubleFunction<V> {
T self();

T put(double x, V y);


interface ValueInterpolator<V> extends BiFunction<V, V, DoubleFunction<V>> {}
interface Continuous<T extends Interpolator<T, Double> & Continuous<T>>
extends IInterpolator<T, Double>, DoubleUnaryOperator {
default DoubleUnaryOperator invert() {
return Interpolator.invertIt(this.self());
}

default T put(double x, double y) {
return put(x, Double.valueOf(y));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package com.onthegomap.planetiler.util;

import com.carrotsearch.hppc.DoubleArrayList;
import java.util.ArrayList;
import java.util.List;
import java.util.function.DoubleFunction;
import java.util.function.DoubleUnaryOperator;
import org.apache.commons.lang3.ArrayUtils;

public class Interpolator<T extends Interpolator<T, V>, V> implements IInterpolator<T, V> {
private static final ValueInterpolator<Double> INTERPOLATE_NUMERIC =
(a, b) -> t -> a * (1 - t) + b * t;
private static final DoubleUnaryOperator IDENTITY = x -> x;
private final ValueInterpolator<V> valueInterpolator;

DoubleUnaryOperator transform = IDENTITY;
DoubleUnaryOperator reverseTransform = IDENTITY;
boolean clamp = false;
private V defaultValue;
final DoubleArrayList domain = new DoubleArrayList();
final List<V> range = new ArrayList<>();
private DoubleFunction<V> fn;
double minKey = Double.POSITIVE_INFINITY;
double maxKey = Double.NEGATIVE_INFINITY;

protected Interpolator(ValueInterpolator<V> valueInterpolator) {
this.valueInterpolator = valueInterpolator;
}

T setTransforms(DoubleUnaryOperator forward, DoubleUnaryOperator reverse) {
fn = null;
this.transform = forward;
this.reverseTransform = reverse;
return self();
}

@Override
public V apply(double operand) {
if (clamp) {
operand = Math.clamp(operand, minKey, maxKey);
}
if (Double.isNaN(operand)) {
return defaultValue;
}
if (fn == null) {
fn = rescale();
}
return fn.apply(transform.applyAsDouble(operand));
}

public double applyAsDouble(double operand) {
return apply(operand) instanceof Number n ? n.doubleValue() : Double.NaN;
}

private DoubleFunction<V> rescale() {
if (domain.size() > 2) {
int j = Math.min(domain.size(), range.size()) - 1;
DoubleUnaryOperator[] d = new DoubleUnaryOperator[j];
DoubleFunction<V>[] r = new DoubleFunction[j];
int i = -1;

double[] domainItems = new double[domain.size()];
for (int k = 0; k < domainItems.length; k++) {
domainItems[k] = transform.applyAsDouble(domain.get(k));
}
List<V> rangeItems = domainItems[j] < domainItems[0] ? range.reversed() : range;

// Reverse descending domains.
if (domainItems[j] < domainItems[0]) {
ArrayUtils.reverse(domainItems);
}

while (++i < j) {
d[i] = normalize(domainItems[i], domainItems[i + 1]);
r[i] = valueInterpolator.apply(rangeItems.get(i), rangeItems.get(i + 1));
}

return x -> {
int ii = bisect(domainItems, x, 1, j) - 1;
return r[ii].apply(d[ii].applyAsDouble(x));
};
} else {
double d0 = transform.applyAsDouble(domain.get(0)), d1 = transform.applyAsDouble(domain.get(1));
V r0 = range.get(0), r1 = range.get(1);
boolean reverse = d1 < d0;
final double dlo = reverse ? d1 : d0;
final double dhi = reverse ? d0 : d1;
final V rlo = reverse ? r1 : r0;
final V rhi = reverse ? r0 : r1;
DoubleUnaryOperator normalize = normalize(dlo, dhi);
DoubleFunction<V> interpolate = valueInterpolator.apply(rlo, rhi);
return x -> interpolate.apply(normalize.applyAsDouble(x));
}
}

private static int bisect(double[] a, double x, int lo, int hi) {
if (lo < hi) {
do {
int mid = (lo + hi) >>> 1;
if (a[mid] <= x)
lo = mid + 1;
else
hi = mid;
} while (lo < hi);
}
return lo;
}

private static DoubleUnaryOperator interpolate(double a, double b) {
return t -> a * (1 - t) + b * t;
}

private static DoubleUnaryOperator normalize(double a, double b) {
double delta = b - a;
return delta == 0 ? x -> 0.5 : Double.isNaN(delta) ? x -> Double.NaN : x -> (x - a) / delta;
}

@SuppressWarnings("unchecked")
public T self() {
return (T) this;
}

public T clamp(boolean clamp) {
this.clamp = clamp;
return self();
}

public T defaultValue(V value) {
this.defaultValue = value;
return self();
}

public T put(double stop, V value) {
fn = null;
minKey = Math.min(stop, minKey);
maxKey = Math.max(stop, maxKey);
domain.add(stop);
range.add(value);
return self();
}

// TODO
// private static class Inverted extends Interpolator<Inverted, Double> {}

public static <T extends Interpolator<T, ? extends Number>> DoubleUnaryOperator invertIt(
Interpolator<T, ? extends Number> interpolator) {
var result = linear();
int j = Math.min(interpolator.domain.size(), interpolator.range.size());
for (int i = 0; i < j; i++) {
result.put(interpolator.range.get(i).doubleValue(),
interpolator.transform.applyAsDouble(interpolator.domain.get(i)));
}
DoubleUnaryOperator retVal =
interpolator.reverseTransform == IDENTITY ? result::applyAsDouble :
x -> interpolator.reverseTransform.applyAsDouble(result.apply(x));
return interpolator.clamp ? retVal.andThen(x -> Math.clamp(x, interpolator.minKey, interpolator.maxKey)) : retVal;
}

public static class Power<T extends Power<T, V>, V> extends Interpolator<T, V> {

public Power(ValueInterpolator<V> valueInterpolator) {
super(valueInterpolator);
}

public T exponent(double exponent) {
double inverse = 1d / exponent;
return setTransforms(x -> Math.pow(x, exponent), y -> Math.pow(y, inverse));
}

public static class Numeric extends Power<Numeric, Double> implements IInterpolator.Continuous<Numeric> {

public Numeric(ValueInterpolator<Double> valueInterpolator) {
super(valueInterpolator);
}
}
public static class Other<V> extends Power<Other<V>, V> {

public Other(ValueInterpolator<V> valueInterpolator) {
super(valueInterpolator);
}
}
}

public static class Log<T extends Log<T, V>, V> extends Interpolator<T, V> {

public Log(ValueInterpolator<V> valueInterpolator) {
super(valueInterpolator);
}

private static DoubleUnaryOperator log(double base) {
double logBase = Math.log(base);
return x -> Math.log(x) / logBase;
}

public T base(double base) {
DoubleUnaryOperator forward =
base == 10d ? Math::log10 :
base == Math.E ? Math::log :
log(base);
DoubleUnaryOperator reverse =
base == Math.E ? Math::exp :
x -> Math.pow(base, x);
return setTransforms(forward, reverse);
}

public static class Numeric extends Log<Log.Numeric, Double> implements IInterpolator.Continuous<Log.Numeric> {

public Numeric(ValueInterpolator<Double> valueInterpolator) {
super(valueInterpolator);
}
}
public static class Other<V> extends Log<Log.Other<V>, V> {

public Other(ValueInterpolator<V> valueInterpolator) {
super(valueInterpolator);
}
}
}

public static class Linear<T extends Linear<T, V>, V> extends Interpolator<T, V> {

public Linear(ValueInterpolator<V> valueInterpolator) {
super(valueInterpolator);
}

public static class Numeric extends Linear<Linear.Numeric, Double>
implements IInterpolator.Continuous<Linear.Numeric> {

public Numeric(ValueInterpolator<Double> valueInterpolator) {
super(valueInterpolator);
}
}
public static class Other<V> extends Linear<Linear.Other<V>, V> {

public Other(ValueInterpolator<V> valueInterpolator) {
super(valueInterpolator);
}
}
}

public static Linear.Numeric linear(ValueInterpolator<Double> valueInterpolator) {
return new Linear.Numeric(valueInterpolator);
}

public static Linear.Numeric linear() {
return new Linear.Numeric(INTERPOLATE_NUMERIC);
}

public static Log.Numeric log(ValueInterpolator<Double> valueInterpolator) {
return new Log.Numeric(valueInterpolator).base(10);
}

public static Log.Numeric log() {
return log(INTERPOLATE_NUMERIC).base(10);
}


// TODO specialized double implementation without boxing (interpolator, values?) ?
// TODO set special args before returning reusable thing? forget self type

public static Power.Numeric npower(ValueInterpolator<Double> valueInterpolator) {
return new Power.Numeric(valueInterpolator).exponent(1);
}

public static Power.Numeric power() {
return npower(INTERPOLATE_NUMERIC).exponent(1);
}

public static Power.Numeric sqrt() {
return power().exponent(0.5);
}
}
Loading