Skip to content

Fix log/symlog tick positions and compute linthresh from data smallest real value#56

Closed
patrickoleary wants to merge 2 commits intomasterfrom
fix/linthresh
Closed

Fix log/symlog tick positions and compute linthresh from data smallest real value#56
patrickoleary wants to merge 2 commits intomasterfrom
fix/linthresh

Conversation

@patrickoleary
Copy link
Copy Markdown
Member

  • Compute linthresh from min abs non-zero value in data array (zero-copy, vectorized O(n))
  • Fix symlog function to standard formulation: sign(v) * log10(1 + |v|/linthresh)
  • Fix log/symlog tick positions to use normalized positions on linear colorbar
  • Fix symlog tick values to use powers of 10 matching LUT breakpoints
  • Capture linear colorbar image before any log/symlog LUT transforms
  • Pass linthresh once from data through all functions that need it

@patrickoleary
Copy link
Copy Markdown
Member Author

Ok I fixed the colormaps and the tick marks and then I made your suggested fix to linthresh, but I don't tink you want this. Below is the correct method for what you asked me to create. It says remove 0 and machine error zeroes. then it finds the next real value to set as linthresh.

def calculate_linthresh(data):
"""Calculate the linear threshold for symlog scaling.

Excludes machine-error zeros (values within ±2*eps of the data dtype),
then returns min(abs(valid)).

Operates on the original array without copies.

Args:
    data: numpy array of data values

Returns:
    linthresh value (float), or 1.0 if no valid values exist
"""
eps = np.finfo(data.dtype).eps
threshold = 2 * eps

# Find min |x| > threshold without allocating a copy.
# Using where= runs as a tight vectorized C loop, roughly 2-3 orders
# of magnitude faster than a Python for loop.
min_pos = np.nanmin(data, where=data > threshold, initial=np.inf)
# For negatives: max(data) where data < -threshold gives closest to zero
max_neg = np.nanmax(data, where=data < -threshold, initial=-np.inf)
min_abs = min(min_pos, -max_neg)

if min_abs == np.inf:
    linthresh = 1.0
else:
    linthresh = float(min_abs)

return linthresh

Here is why this is a problem.

If you only have big positive numbers (10^5 to 10^10, or what ever), then the linear section 0 to linthresh will go all the way to the smallest value which is a big number (10^5), and connect to the logarithmic section.

When you approximate a line using a discrete linear section connected to a discrete nonlinear section, the meeting point creates a kink—a non-differentiable point where the slope changes instantly. It's continuous but non-differentiable.

Mapping this to a colormap creates a gradient discontinuity (because it is non-differentiable at this point). Even though the data is continuous, the sudden change in the rate of color transition triggers:
Mach Bands: A visual illusion where your brain "sees" a faint line or glow at the transition, creating a fake feature.
False Banding: Artificial ridges that appear as structural boundaries in the data, even if the underlying values are smooth.

I think what you want is to move all the color to between 10^5 and 10^10. this can be done by clamping the data which already have (setting the data range).

Also, the example image you have provided uses a discrete colormap where the values between 10^5 and 10^6 are one color, and so on. We do not have this, but we could have this if you want. I think we should iterate to get you exactly what you are looking for.

@patrickoleary
Copy link
Copy Markdown
Member Author

this is much closer. I would merge these changes, but I wouldn't close the issue. it fixes a number of problems, but in the end it doesn't match the example image discrete colormap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants