///|
priv struct AsinhGridPoint {
xs : Double
pows : Double
value : Double
}
///|
priv struct AsinhGridContext {
line : AsinhGridLine
width : Double
base : Double
logarithm : Double
cache : Map[UInt64, AsinhGridPoint]
}
///|
fn asinh_grid_round(value : Double) -> Double {
if value.is_nan() ||
value.is_inf() ||
Double::abs(value) >= 4503599627370496.0 {
return value
}
let lower = value.floor()
let fraction = value - lower
let rounded = if fraction > 0.5 ||
(fraction == 0.5 && (lower.to_int64() & 1L) != 0L) {
lower + 1.0
} else {
lower
}
if rounded == 0.0 {
0.0 * value
} else {
rounded
}
}
///|
fn AsinhGridContext::point(
self : AsinhGridContext,
y : Double,
) -> AsinhGridPoint {
let key = y.reinterpret_as_uint64()
if self.cache.get(key) is Some(value) {
return value
}
let xs = self.width * asinh_math_sinh(y / self.width)
let point = if self.base > 1.0 {
let sign = if xs.is_nan() {
xs
} else if xs > 0.0 {
1.0
} else if xs < 0.0 {
-1.0
} else {
0.0
}
let phase = (log_grid_natural_log(Double::abs(xs)) / self.logarithm).floor()
let pows = sign * log_grid_numpy_power(self.base, phase)
{ xs, pows, value: pows, }
} else {
// np.where's zero predicate is ys==0, not the possibly underflowed xs.
let pows = if y == 0.0 {
1.0
} else {
log_grid_numpy_power(10.0, asinh_math_log10(Double::abs(xs)).floor())
}
{ xs, pows, value: pows * asinh_grid_round(xs / pows), }
}
self.cache[key] = point
point
}
///|
fn AsinhGridContext::value(
self : AsinhGridContext,
run : AsinhGridRun,
offset : Int64,
factor : Double,
) -> Double {
let point = self.point(run.at(self.line, offset))
if self.base > 1.0 {
point.pows * factor
} else {
point.value
}
}
///|
// The source's late truth test distinguishes numeric sequences, ndarrays,
// and scalar inputs. Only the selected np.outer path consumes truthy text.
fn asinh_grid_subs(
raw : AsinhSubs?,
) -> (Array[Double], Bool) raise @core.DataError {
match raw {
None => ([1.0], false)
Some(Sequence(values)) =>
if values.is_empty() {
([1.0], false)
} else {
(values, true)
}
Some(Tuple(values)) =>
if values.length() == 0 {
([1.0], false)
} else {
(values.values(), true)
}
Some(NdArray(values)) => {
if values.length() != 1 {
raise log_grid_error("Asinh ndarray truth value is ambiguous")
}
let value = values.get(0)
if value == 0.0 {
([1.0], false)
} else {
([value], true)
}
}
Some(Number(value)) =>
if value == 0.0 {
([1.0], false)
} else {
([value], true)
}
Some(Text(value)) =>
if value == "" {
([1.0], false)
} else {
raise log_grid_error(
"Asinh outer product cannot multiply numeric values by text",
)
}
}
}
///|
// On a finite same-sign ys run, abs(ys) increases in the selected direction.
// Class0/1/2 means NaN below / numeric / NaN above its contiguous numeric
// interval. Special bases and Inf/zero subs retain their distinct IEEE cases.
fn AsinhGridContext::classify(
self : AsinhGridContext,
run : AsinhGridRun,
offset : Int64,
factor : Double,
) -> Int32 {
let point = self.point(run.at(self.line, offset))
if self.base > 1.0 {
if self.base.is_inf() {
if point.xs == 0.0 {
0
} else if point.xs.is_inf() {
2
} else {
1
}
} else if factor.is_inf() {
if point.pows == 0.0 {
0
} else {
1
}
} else if factor == 0.0 {
if point.pows.is_inf() {
2
} else {
1
}
} else {
1
}
} else if point.xs.is_inf() {
2
} else if point.value.is_nan() {
0
} else {
1
}
}
///|
fn named_asinh_locator_ticks(
locator : AsinhLocator,
lower : Double,
upper : Double,
) -> Array[Double] raise @core.DataError {
let width = locator.linear_width
// Preserve the source's two separate divisions and width multiplications.
let first = width * asinh_math_asinh(lower / width)
let last = width * asinh_math_asinh(upper / width)
let count = asinh_grid_count(locator.numticks)
let line = asinh_grid_line(first, last, count)
let runs = asinh_grid_runs(line)
let context : AsinhGridContext = {
line,
width,
base: locator.base,
logarithm: log_grid_natural_log(locator.base),
cache: Map([]),
}
let (factors, outer) = if locator.base > 1.0 {
asinh_grid_subs(locator.subs)
} else {
([1.0], false)
}
let mut row_count = 0L
for run in runs {
row_count += run.length
}
let full_shape = @bigint.BigInt::from_int64(row_count) *
@bigint.BigInt::from_int(factors.length())
if full_shape >= @bigint.BigInt::from_int(1) << 60 {
raise log_grid_error("Asinh full outer array exceeds the NumPy byte size")
}
ignore(outer)
let heap : Array[AsinhGridCursor] = []
let mut nan_count = 0L
for run in runs {
for subdivision, factor in factors {
let multiplier = factors.length().to_int64()
if run.constant is Some(_) {
let value = context.value(run, 0L, factor)
if value.is_nan() {
nan_count += run.length
} else {
asinh_grid_heap_push(
heap,
asinh_grid_cursor(
context, run, 0L, 1L, false, factor, subdivision, multiplier, 0L,
),
)
}
} else if factor.is_nan() && locator.base > 1.0 {
nan_count += run.length
} else {
let increasing = Double::abs(run.at(line, 0L)) <=
Double::abs(run.at(line, run.length - 1L))
let position = fn(index : Int64) {
if increasing {
index
} else {
run.length - 1L - index
}
}
let first_numeric = asinh_grid_boundary(run.length, index => {
context.classify(run, position(index), factor) >= 1
})
let after_numeric = asinh_grid_boundary(run.length, index => {
context.classify(run, position(index), factor) >= 2
})
nan_count += first_numeric + run.length - after_numeric
if after_numeric > first_numeric {
let (first, last) = if increasing {
(first_numeric, after_numeric)
} else {
(run.length - after_numeric, run.length - first_numeric)
}
asinh_grid_push_numeric(
heap, context, run, first, last, factor, subdivision, multiplier,
)
}
}
}
}
let values : Array[Double] = []
while !heap.is_empty() && values.length() < 100000 {
let cursor = asinh_grid_heap_pop(heap)
if values.is_empty() || values[values.length() - 1] != cursor.value {
values.push(cursor.value)
}
if cursor.next < cursor.last - cursor.first {
asinh_grid_heap_push(
heap,
asinh_grid_cursor(
context,
cursor.run,
cursor.first,
cursor.last,
cursor.reverse,
cursor.factor,
cursor.subdivision,
cursor.multiplier,
cursor.next,
),
)
}
}
if values.length().to_int64() + nan_count < 2L {
let fallback = asinh_grid_line(lower, upper, count)
return Array::makei(
if fallback.count > 100000L {
100000
} else {
fallback.count.to_int()
},
index => fallback.at(index.to_int64()),
)
}
// Deterministic profile for Python's genuinely nondeterministic NaN set order:
// numeric ascending, earliest flattened zero, all individual NaNs last.
let remaining = Int64::min(nan_count, (100000 - values.length()).to_int64()).to_int()
for _ in 0..