Repository: facelessuser/coloraide (language: python)

Issue:
Spaces like HCT should respond with their own achromatics
I think I've been coming to this conclusion for a while.

Currently, we provide a fancy spline that calculates the actual achromatic response, based on the original white point, but many of these spaces are actually converting colors based on an adapting luminance and background luminance. The achromatic response where chroma is zero doesn't look the same as when you discount illuminance, which is exactly what you expect.

I'm thinking that all the fancy overhead to preserve this true achromatic line is wasted effort. Much like RYB where we return its achromatic colors, maybe we should do the same for all these spaces. It simplifies the code, probably makes it faster, and probably gives results not too different than what people would expect. I imagine most probably can't tell that HCT white is slightly less white due to accounting for adapting luminance. It also makes it much easier for people to quickly crate a variant and change the background and adapting luminance.


Discussion / hints:

Fix patch:
diff --git a/coloraide/spaces/achromatic.py b/coloraide/spaces/achromatic.py
deleted file mode 100644
index 1662e217..00000000
--- a/coloraide/spaces/achromatic.py
+++ /dev/null
@@ -1,211 +0,0 @@
-"""Tools for dynamic achromatic response."""
-from __future__ import annotations
-from .. import algebra as alg
-import bisect
-from typing import Any
-from ..types import Vector, MatrixLike, Matrix  # noqa: F401
-from abc import ABCMeta, abstractmethod
-import math
-
-
-class Achromatic(metaclass=ABCMeta):
-    """Calculate a spline that follows a color's achromatic response."""
-
-    L_IDX = 0
-    C_IDX = 1
-    H_IDX = 2
-
-    def __init__(
-        self,
-        data: Matrix | None = None,
-        threshold_upper: float = math.inf,
-        threshold_lower: float = math.inf,
-        threshold_cutoff: float = math.inf,
-        spline: str = 'linear',
-        mirror: bool = False,
-        negative: bool = False,
-        **kwargs: Any
-    ) -> None:
-        """
-        Initialize.
-
-        `tuning`: Either a dictionary of `low`, `mid`, and `high`, each specifying a start, end, step, and scale
-            used to build a portion of the spline representing the achromatic response or a list containing the
-            the pre-calculated spline points.
-        `threshold_upper`: threshold above achromatic curve.
-        `threshold_lower`: threshold below achromatic curve.
-        `threshold_cutoff`: threshold of chroma above which we will assume colors are not achromatic.
-        `spline`: spline type.
-        `mirror`: Mirror response across lightness axis at zero.
-        """
-
-        self.mirror = mirror
-        self.threshold_upper = threshold_upper
-        self.threshold_lower = threshold_lower
-        self.threshold_cutoff = threshold_cutoff
-        self.negative = negative
-
-        self.domain = []  # type: Vector
-        self.min_colorfulness = 1e10
-        self.min_lightness = 1e10
-        self.spline_type = spline
-
-        # Create a spline that maps the achromatic range for the SDR range
-        if data is not None:
-            self.setup_achromatic_response(data, **kwargs)
-
-    def dump(self) -> MatrixLike | None:  # pragma: no cover
-        """Dump data points."""
-
-        if self.spline_type == 'linear':
-            return list(zip(*self.spline.points))
-        else:
-            # Strip off the data points used to coerce the spline through the end.
-            return list(zip(*self.spline.points))[1:-1]
-
-    @abstractmethod
-    def convert(self, coords: Vector, **kwargs: Any) -> Vector:
-        """Convert an sRGB color to the desired space."""
-
-    def calc_achromatic_response(
-        self,
-        parameters: list[tuple[int, int, int, float]],
-        **kwargs: Any
-    ) -> None:  # pragma: no cover
-        """
-        Calculate the achromatic response.
-
-        Used to precalculate the best response.
-        """
-
-        points = []  # type: Matrix
-        for segment in parameters:
-            start, end, step, scale = segment
-            for p in range(start, end, step):
-                color = self.convert([(-p if self.negative else p) / scale] * 3, **kwargs)
-                l, c, h = color[self.L_IDX], color[self.C_IDX], color[self.H_IDX]
-                if self.negative:
-                    la = abs(l)
-                    if la < self.min_lightness:
-                        self.min_lightness = la
-                else:
-                    if l < self.min_lightness:
-                        self.min_lightness = l
-                if c < self.min_colorfulness:
-                    self.min_colorfulness = c
-                self.domain.append(l)
-                points.append([l, c, h % 360])
-        if self.negative:
-            self.domain.sort()
-            points.sort()
-        self.spline = alg.interpolate(points, method=self.spline_type)
-        self.hue = self.convert([1] * 3, **kwargs)[self.H_IDX] % 360
-        self.ihue = (self.hue - 180) % 360
-
-    def setup_achromatic_response(
-        self,
-        tuning: Matrix,
-        **kwargs: Any
-    ) -> None:
-        """Setup the achromatic response."""
-
-        points = []  # type: Matrix
-        for entry in tuning:
-            l, c, h = entry
-            if self.negative:
-                la = abs(l)
-                if la < self.min_lightness:
-                    self.min_lightness = la
-            else:
-                if l < self.min_lightness:
-                    self.min_lightness = l
-            if c < self.min_colorfulness:
-                self.min_colorfulness = c
-            points.append([l, c, h])
-            self.domain.append(l)
-        self.spline = alg.interpolate(points, method=self.spline_type)
-        self.hue = self.convert([1] * 3, **kwargs)[self.H_IDX] % 360
-        self.ihue = (self.hue - 180) % 360
-
-    def scale(self, point: float) -> float:
-        """Scale the lightness to match the range."""
-
-        if point <= self.domain[0]:
-            point = (point - self.domain[0]) / (self.domain[-1] - self.domain[0])
-        elif point >= self.domain[-1]:
-            point = 1.0 + (point - self.domain[-1]) / (self.domain[-1] - self.domain[0])
-        else:
-            regions = len(self.domain) - 1
-            size = (1 / regions)
-            index = 0
-            adjusted = 0.0
-            index = bisect.bisect(self.domain, point) - 1
-            a, b = self.domain[index:index + 2]
-            l = b - a
-            adjusted = ((point - a) / l) if l else 0.0
-            point = size * index + (adjusted * size)
-        return point
-
-    def get_ideal_chroma(self, l: float) -> float:
-        """Get the ideal chroma."""
-
-        if math.isnan(l):
-            return 0.0
-
-        elif self.mirror and l < 0.0:
-            return self.spline(self.scale(abs(l)))[1]
-
-        return self.spline(self.scale(l))[1]
-
-    def get_ideal_hue(self, l: float, c: float = alg.NaN) -> float:
-        """Get the ideal chroma."""
-
-        if math.isnan(l):
-            return 0.0
-
-        # Adjust hue if lightness is negative and/or chroma is negative.
-        if self.mirror and l < 0.0:
-            return (self.spline(self.scale(abs(l)))[2] - 180 + (180.0 if c < 0 else 0.0)) % 360
-        elif c < 0:
-            return (self.spline(self.scale(l))[2] + 180) % 360
-
-        return self.spline(self.scale(l))[2]
-
-    def get_ideal_ab(self, l: float) -> tuple[float, float]:
-        """Get the ideal rectangular form of chroma and hue, the components a and b."""
-
-        if math.isnan(l):
-            return 0.0, 0.0
-
-        return alg.polar_to_rect(self.get_ideal_chroma(l), self.get_ideal_hue(l))
-
-    def test(self, l: float, c: float, h: float) -> bool:
-        """Test if the current color is achromatic."""
-
-        # If colorfulness is past this limit, we'd have to have a lightness
-        # so high, that our test has already broken down.
-        if (
-            abs(c) > self.threshold_cutoff or
-            (not self.mirror and ((not self.negative and l < 0.0) or (self.negative and l > 0.0)))
-        ):
-            return False
-
-        # If we are higher than 1, we are extrapolating;
-        # otherwise, use the spline.
-        flip = self.mirror and l < 0.0
-        la = abs(l)
-        point = self.scale(la if flip else l)
-        if la < self.min_lightness and c < self.min_colorfulness:  # pragma: no cover
-            return True
-        else:
-            c2, h2 = self.spline(point)[1:]
-            if flip:
-                h2 = (h2 - 180) % 360
-        diff = c2 - c
-        hdiff = abs(h % 360 - h2)
-        if hdiff > 180:  # pragma: no cover
-            hdiff = 360 - hdiff
-        return (
-            ((diff >= 0 and diff < self.threshold_upper) or (diff < 0 and abs(diff) < self.threshold_lower)) and
-            (c2 < 1e-5 or hdiff < 0.01)
-        )
diff --git a/coloraide/spaces/cam16.py b/coloraide/spaces/cam16.py
deleted file mode 100644
index 4989c0b7..00000000
--- a/coloraide/spaces/cam16.py
+++ /dev/null
@@ -1,113 +0,0 @@
-"""
-CAM16 class.
-
-https://www.imaging.org/site/PDFS/Papers/2000/PICS-0-81/1611.pdf
-https://observablehq.com/@jrus/cam16
-https://arxiv.org/abs/1802.06067
-https://doi.org/10.1002/col.22131
-"""
-from __future__ import annotations
-import math
-from .cam16_jmh import CAM16JMh, xyz_d65_to_cam16, cam16_to_xyz_d65, Environment
-from ..spaces import Space, Labish
-from ..cat import WHITES
-from ..channels import Channel, FLG_MIRROR_PERCENT
-from .. import util
-from ..types import Vector
-from .. import algebra as alg
-
-
-def cam16_jmh_to_cam16_jab(jmh: Vector, env: Environment) -> Vector:
-    """Translate a CAM16 JMh to Jab of the same viewing conditions."""
-
-    J, M, h = jmh
-
-    if J == 0.0:
-        return [0.0, 0.0, 0.0]
-
-    # Account for negative colorfulness by reconverting
-    if M < 0:
-        cam16 = xyz_d65_to_cam16(cam16_to_xyz_d65(J=J, M=M, h=h, env=env), env=env)
-        J, M, h = cam16[0], cam16[5], cam16[2]
-
-    return [
-        J,
-        M * math.cos(math.radians(h)),
-        M * math.sin(math.radians(h))
-    ]
-
-
-def cam16_jab_to_cam16_jmh(jab: Vector) -> Vector:
-    """Translate a CAM16 Jab to JMh of the same viewing conditions."""
-
-    J, a, b = jab
-
-    if J == 0.0:
-        return [0.0, 0.0, 0.0]
-
-    M = math.sqrt(a ** 2 + b ** 2)
-    h = math.degrees(math.atan2(b, a))
-
-    return [J, M, util.constrain_hue(h)]
-
-
-class CAM16(Labish, Space):
-    """CAM16 class (Jab)."""
-
-    BASE = "cam16-jmh"
-    NAME = "cam16"
-    SERIALIZE = ("--cam16",)
-    CHANNELS = (
-        Channel("j", 0.0, 100.0),
-        Channel("a", -90.0, 90.0, flags=FLG_MIRROR_PERCENT),
-        Channel("b", -90.0, 90.0, flags=FLG_MIRROR_PERCENT)
-    )
-    CHANNEL_ALIASES = {
-        "lightness": "j"
-    }
-    WHITE = WHITES['2deg']['D65']
-    # Use the same environment as CAM16JMh
-    ENV = CAM16JMh.ENV
-    ACHROMATIC = CAM16JMh.ACHROMATIC
-    INV_ACHROMATIC = CAM16JMh.INV_ACHROMATIC
-
-    def resolve_channel(self, index: int, coords: Vector) -> float:
-        """Resolve channels."""
-
-        if index in (1, 2):
-            # Assume black if J is below zero
-            J = coords[0]
-
-            achromatic = self.INV_ACHROMATIC if J < 0 else self.ACHROMATIC
-
-            if not math.isnan(coords[index]):
-                return coords[index]
-
-            return achromatic.get_ideal_ab(J)[index - 1]
-
-        value = coords[index]
-        return self.channels[index].nans if math.isnan(value) else value
-
-    def is_achromatic(self, coords: Vector) -> bool:
-        """Check if color is achromatic."""
-
-        j = coords[0]
-        m, h = alg.rect_to_polar(coords[1], coords[2])
-
-        if j == 0.0:
-            return True
-
-        if j < 0.0:
-            return self.INV_ACHROMATIC.test(j, m, h)
-
-        return self.ACHROMATIC.test(j, m, h)
-
-    def to_base(self, coords: Vector) -> Vector:
-        """To CAM16 JMh from CAM16."""
-
-        return cam16_jab_to_cam16_jmh(coords)
-
-    def from_base(self, coords: Vector) -> Vector:
-        """From CAM16 JMh to CAM16."""
-
-        return cam16_jmh_to_cam16_jab(coords, self.ENV)
diff --git a/coloraide/spaces/cam16_jmh.py b/coloraide/spaces/cam16_jmh.py
index 17b8941d..5b4ce0db 100644
--- a/coloraide/spaces/cam16_jmh.py
+++ b/coloraide/spaces/cam16_jmh.py
@@ -14,11 +14,8 @@
 from ..spaces import Space, LChish
 from ..cat import WHITES, CAT16
 from ..channels import Channel, FLG_ANGLE
-from .achromatic import Achromatic as _Achromatic
-from .srgb_linear import lin_srgb_to_xyz
-from .srgb import lin_srgb
-from ..types import Vector, VectorLike, Matrix
-from typing import Any
+from .lch import ACHROMATIC_THRESHOLD
+from ..types import Vector, VectorLike
 
 # CAT16
 M16 = CAT16.MATRIX
@@ -46,261 +43,6 @@
     "H": (0.0, 100.0, 200.0, 300.0, 400.0)
 }
 
-ACHROMATIC_RESPONSE = [
-    [0.5197637353103578, 0.10742101902546781, 209.53702017876432],
-    [0.7653410752577902, 0.14285252204838345, 209.53699586564701],
-    [0.9597225272459006, 0.1674639325209385, 209.5369779985614],
-    [1.1268800051398011, 0.18685008110835863, 209.5369633468102],
-    [1.2763267654963262, 0.2030639541700499, 209.53695070009815],
-    [1.8790377054651906, 0.26061543207726645, 209.53690294574977],
-    [2.3559753209404573, 0.2998668559412108, 209.53686786031403],
-    [2.7660324716814633, 0.3305058430743193, 209.536839093849],
-    [3.1325805945142164, 0.3559891904115732, 209.53681426762876],
-    [3.4678031173571573, 0.3779903818931045, 209.5367921887862],
-    [3.7790286254197216, 0.397459500662546, 209.53677216077287],
-    [4.071081036208187, 0.41499276041595823, 209.53675373607373],
-    [4.35987940988476, 0.4317047383578854, 209.53673583657556],
-    [4.653923368359191, 0.44814375233486375, 209.53671791170922],
-    [7.833618587426542, 0.5996289140723937, 209.53653852944092],
-    [11.379933581738413, 0.7341977581705729, 209.53635903741286],
-    [15.226965773377026, 0.857259036676519, 209.53617955384075],
-    [19.331425543357224, 0.9717687075452716, 209.53600014936998],
-    [23.662223781347393, 1.0795574256327514, 209.5358208710024],
-    [28.195712492409086, 1.181856050998015, 209.5356417521982],
-    [32.91316356733729, 1.2795417603936137, 209.53546281794857],
-    [37.799294675334956, 1.3732674055098613, 209.53528408756415],
-    [42.841343691205466, 1.4635354939944947, 209.5351055763844],
-    [48.028455187972774, 1.5507433232034882, 209.53492729681835],
-    [53.35125605180922, 1.6352119585004592, 209.53474925909882],
-    [58.80155160494701, 1.7172056265103606, 209.53457147176655],
-    [64.37210171522501, 1.7969451471063127, 209.53439394202456],
-    [70.05645182832635, 1.8746175094579138, 209.53421667601953],
-    [75.8488028092011, 1.95038286970608, 209.53403967901312],
-    [81.74390888832981, 2.0243797747219405, 209.5338629555596],
-    [87.73699639870503, 2.096729134902292, 209.53368650960735],
-    [93.82369818202439, 2.1675372954936383, 209.53351034459058],
-    [100.0, 2.2368984457705743, 209.53333446353417],
-    [106.26219627903436, 2.304896533525034, 209.53315886906827],
-    [112.60685320678104, 2.37160680430044, 209.5329835635175],
-    [119.03077768851209, 2.4370970520495017, 209.53280854892373],
-    [125.53099102420309, 2.501428645093027, 209.53263382708093],
-    [132.1047064258276, 2.5646573751376196, 209.532459399577],
-    [138.74930968646498, 2.626834165511834, 209.53228526779787],
-    [145.46234245739691, 2.6880056663241145, 209.5321114329661],
-    [152.24148769946316, 2.748214758003532, 209.5319378961533],
-    [159.08455695969053, 2.807500980008346, 209.53176465830455],
-    [165.98947919010237, 2.865900897958051, 209.53159172021918],
-    [172.95429087732893, 2.92344841973748, 209.53141908260915],
-    [179.9771272925688, 2.980175069050048, 209.53124674607443],
-    [187.05621470411134, 3.0361102232687336, 209.5310747111302],
-    [194.18986342088985, 3.0912813211607015, 209.53090297820904],
-    [201.3764615567875, 3.145714045061113, 209.5307315476577],
-    [208.6144694227416, 3.199432481261756, 209.53056041976922],
-    [215.90241446789472, 3.252459261745779, 209.5303895947692],
-    [223.23888670275286, 3.304815689873112, 209.53021907281482],
-    [230.62253454702395, 3.3565218522006455, 209.53004885402189],
-    [238.05206105290907, 3.4075967182791853, 209.5298789384579],
-    [245.52622046139462, 3.458058229985187, 209.52970932613115],
-    [253.04381505480683, 3.507923381705505, 209.52954001701696],
-    [260.6036922737095, 3.5572082925096393, 209.52937101105175],
-    [268.20474207032214, 3.605928271271525, 209.52920230813686],
-    [275.8458944741247, 3.654097875574637, 209.52903390813748],
-    [283.52611734829725, 3.7017309651178785, 209.52886581088453],
-    [291.2444143182101, 3.7488407502396903, 209.52869801618388],
-    [298.9998228553806, 3.795439836103318, 209.52853052381926],
-    [306.7914125022303, 3.841540263013781, 209.52836333353144],
-    [314.61828322461565, 3.8871535432701356, 209.52819644505152],
-    [322.479563880563, 3.9322906949207233, 209.52802985809194],
-    [330.37441079487513, 3.9769622727358835, 209.52786357233052],
-    [338.30200643038665, 4.021178396670769, 209.52769758743355],
-    [346.2615581476034, 4.064948778071774, 209.527531903052],
-    [354.2522970453064, 4.108282743840221, 209.52736651881136],
-    [362.2734768754491, 4.151189258746347, 209.52720143432805],
-    [370.32437302633326, 4.193676946068329, 209.52703664919864],
-    [378.40428156863675, 4.235754106704258, 209.52687216301322],
-    [386.5125183593773, 4.2774287369029045, 209.52670797533614],
-    [394.648418199364, 4.318708544722146, 209.5265440857335],
-    [402.8113340400954, 4.359600965341976, 209.52638049373465],
-    [411.00063623642933, 4.400113175309797, 209.52621719889768],
-    [419.2157118416773, 4.440252105831943, 209.52605420073294],
-    [427.45596394207547, 4.480024455165197, 209.52589149876238],
-    [435.7208110278373, 4.519436700202084, 209.52572909248678],
-    [444.00968639824373, 4.558495107300929, 209.52556698141063],
-    [452.32203759843054, 4.597205742430075, 209.52540516500954],
-    [460.65732588572826, 4.635574480668486, 209.52524364277534],
-    [469.01502572358646, 4.67360701512636, 209.52508241417655],
-    [477.39462430127537, 4.711308865315819, 209.52492147868603],
-    [485.79562107768834, 4.7486853850221635, 209.5247608357571],
-    [494.2175273477135, 4.78574176970701, 209.5246004848463],
-    [502.65986582975273, 4.822483063478878, 209.52444042540557],
-    [511.12217027307815, 4.858914165665327, 209.52428065686505],
-    [519.603985083808, 4.895039837005332, 209.52412117867837],
-    [528.1048649683819, 4.930864705501955, 209.52396199027478],
-    [536.6243745934894, 4.966393271948785, 209.52380309107085],
-    [545.1620882614892, 5.00162991514904, 209.52364448050218],
-    [553.7175896004151, 5.036578896863506, 209.52348615798303],
-    [562.2904712677357, 5.071244366487241, 209.52332812293528],
-    [570.8803346670916, 5.105630365484719, 209.52317037476547],
-    [579.4867896772784, 5.139740831594337, 209.5230129128839],
-    [588.1094543928118, 5.173579602815829, 209.5228557367024],
-    [596.7479548754266, 5.207150421200013, 209.52269884562236],
-    [605.4019249159434, 5.240456936444621, 209.52254223903395],
-    [614.0710058059254, 5.2735027093139655, 209.5223859163477],
-    [622.7548461186354, 5.306291214893902, 209.52222987695998],
-    [631.4531014987923, 5.338825845688801, 209.52207412025294],
-    [640.1654344606808, 5.371109914568736, 209.52191864562857],
-    [648.8915141941957, 5.403146657580616, 209.5217634524664],
-    [657.6310163784137, 5.434939236625219, 209.52160854017026],
-    [666.3836230023236, 5.466490742017722, 209.52145390811216],
-    [675.1490221923619, 5.497804194921784, 209.5212995556808],
-    [683.9269080464271, 5.528882549682189, 209.52114548225762],
-    [692.7169804740557, 5.559728696047723, 209.52099168723132],
-    [701.5189450424705, 5.590345461302892, 209.5208381699748],
-    [710.3325128282281, 5.620735612298451, 209.52068492987357],
-    [719.1574002741951, 5.6509018574012, 209.52053196630135],
-    [727.9933290516212, 5.6808468483554915, 209.52037927864566],
-    [736.8400259270659, 5.710573182071535, 209.52022686627654],
-    [745.6972226339658, 5.7400834023332274, 209.5200747285655],
-    [754.5646557486314, 5.769380001437249, 209.51992286490193],
-    [763.4420665704857, 5.79846542176912, 209.51977127465065],
-    [772.3292010063437, 5.827342057308235, 209.519619957194]
-]  # type: Matrix
-
-INV_ACHROMATIC_RESPONSE = [
-    [-745.6972226339658, 3.9058442577310335, 29.520074728565508],
-    [-735.069838051441, 3.8819263653809566, 29.520257326754717],
-    [-724.4576501804336, 3.8577985580981653, 29.520440320639388],
-    [-713.8611251494627, 3.833457877370439, 29.52062371129394],
-    [-703.2807394924491, 3.808901290613594, 29.520807499801364],
-    [-692.7169804740557, 3.784125688308396, 29.520991687231326],
-    [-682.1703464292635, 3.7591278809827804, 29.521176274649854],
-    [-671.6413471180169, 3.7339045960347192, 29.52136126314039],
-    [-661.1305040957991, 3.708452474389241, 29.52154665374936],
-    [-650.6383511010857, 3.6827680669597007, 29.52173244755951],
-    [-640.1654344606808, 3.6568478309274903, 29.521918645628578],
-    [-629.7123135140142, 3.630688125805234, 29.522105249012025],
-    [-619.2795610575735, 3.6042852092759383, 29.522292258760274],
-    [-608.867763810707, 3.5776352327921503, 29.522479675925247],
-    [-598.4775229041638, 3.5507342369185246, 29.522667501562207],
-    [-588.1094543928114, 3.52357814639877, 29.52285573670506],
-    [-577.7641897941089, 3.4961627649222966, 29.52304438238462],
-    [-567.4423766540311, 3.4684837695697692, 29.52323343964739],
-    [-557.1446791422768, 3.4405367049209086, 29.523422909501317],
-    [-546.8717786787527, 3.4123169767805006, 29.523612792968613],
-    [-536.6243745934894, 3.3838198455090045, 29.523803091070853],
-    [-526.4031848223244, 3.3550404189212553, 29.523993804799613],
-    [-516.2089466409072, 3.3259736447087915, 29.524184935147293],
-    [-506.04241743978673, 3.296614302357983, 29.524376483108842],
-    [-495.90437554360454, 3.266956994517464, 29.524568449660666],
-    [-485.79562107768817, 3.2369961377639362, 29.52476083575679],
-    [-475.7169768856457, 3.2067259527154626, 29.52495364236188],
-    [-465.66928950189845, 3.1761404534389444, 29.52514687041382],
-    [-455.6534301834797, 3.1452334360790952, 29.525340520846825],
-    [-445.6702960058343, 3.1139984666461693, 29.52553459456717],
-    [-435.7208110278373, 3.082428867874411, 29.5257290924868],
-    [-425.8059275317766, 3.0505177050718344, 29.525924015485998],
-    [-415.92662734463676, 3.0182577708558007, 29.52611936443042],
-    [-406.083923247684, 2.9856415686663307, 29.52631514016902],
-    [-396.27886048211604, 2.952661294934431, 29.52651134353625],
-    [-386.51251835937717, 2.9193088197676564, 29.526707975334507],
-    [-376.7860119857034, 2.8855756659908245, 29.526905036355817],
-    [-367.10049411155643, 2.851452986376106, 29.52710252735586],
-    [-357.45715711784163, 2.8169315388520286, 29.52730044906793],
-    [-347.8572351522114, 2.782001659474689, 29.527498802198505],
-    [-338.30200643038665, 2.7466532328995963, 29.52769758743247],
-    [-328.7927957192639, 2.7108756600670354, 29.52789680540236],
-    [-319.33097702071825, 2.6746578227614957, 29.52809645671388],
-    [-309.9179764774579, 2.6379880446668658, 29.52829654193727],
-    [-300.5552755251352, 2.6008540484792033, 29.528497061598884],
-    [-291.24441431821003, 2.563242908568836, 29.528698016188653],
-    [-281.98699546090495, 2.5251409986089803, 29.52889940612333],
-    [-272.7846880790835, 2.486533933485875, 29.529101231797068],
-    [-263.639232274142, 2.447406504709738, 29.529303493528808],
-    [-254.5524440062237, 2.4077426083931366, 29.529506191571432],
-    [-245.52622046139462, 2.367525164712573, 29.529709326130295],
-    [-236.56254596615122, 2.326736027580904, 29.5299128973098],
-    [-227.6634985230424, 2.2853558830037684, 29.53011690515518],
-    [-218.83125705368175, 2.2433641343318333, 29.530321349611842],
-    [-210.0681094504913, 2.2007387722434517, 29.530526230530054],
-    [-201.3764615567875, 2.1574562268776667, 29.530731547657684],
-    [-192.75884721709647, 2.1134911989870173, 29.53093730061302],
-    [-184.21793956692375, 2.0688164663097193, 29.531143488887402],
-    [-175.7565637649753, 2.023402660514716, 29.53135011183324],
-    [-167.37771141282732, 1.9772180089969948, 29.531557168641637],
-    [-159.08455695969053, 1.9302280344282914, 29.531764658304553],
-    [-150.88047645641842, 1.8823952031949056, 29.531972579634196],
-    [-142.76906910766013, 1.8336785115639005, 29.532180931194063],
-    [-134.75418218006106, 1.7840329953780394, 29.532389711302343],
-    [-126.8399399660233, 1.7334091450861648, 29.532598917989905],
-    [-119.03077768851209, 1.6817522025123908, 29.532808548921828],
-    [-111.3314814795425, 1.6290013084445116, 29.533018601410397],
-    [-103.74723589766963, 1.5750884600392587, 29.533229072305296],
-    [-96.28368090402213, 1.5199372229123853, 29.533439957935965],
-    [-88.94698084625801, 1.4634611227411158, 29.53365125402588],
-    [-81.74390888832978, 1.405561612204716, 29.533862955558845],
-    [-74.68195160155422, 1.3461254663212219, 29.534075056667046],
-    [-67.76944030933414, 1.2850213948099434, 29.534287550419478],
-    [-61.01571860238462, 1.222095560637958, 29.534500428593944],
-    [-54.431359810175515, 1.1571655361895847, 29.53471368135187],
-    [-48.028455187972774, 1.0900119706792142, 29.53492729681837],
-    [-41.821005124247854, 1.0203668063332338, 29.5351412604527],
-    [-35.82546562518518, 0.9478961141166822, 29.535355554197068],
-    [-30.061538624119837, 0.8721742118508005, 29.53557015516213],
-    [-24.553364919057078, 0.7926430161985776, 29.535785033575767],
-    [-19.331425543357224, 0.7085451025202562, 29.536000149370004],
-    [-14.435797393047228, 0.6188075378862801, 29.536215446052257],
-    [-9.922309454243656, 0.5218316998784264, 29.536430838352594],
-    [-5.876049367374544, 0.4151475361300732, 29.536646182976344],
-    [-1.8790377054651906, 0.27254960955258617, 29.536902945749794],
-    [-0.5197637353103578, 0.2703043894853694, 29.537020178764333]
-]  # type: Matrix
-
-
-class Achromatic(_Achromatic):
-    """
-    Test if color is achromatic.
-
-    Should work quite well through the SDR range. Can reasonably handle HDR range out to 3
-    which is far enough for anything practical.
-    We use a spline mainly to quickly fit the line in a way we do not have to analyze and tune.
-    """
-
-    L_IDX = 0
-    C_IDX = 1
-    H_IDX = 2
-
-    def __init__(
-        self,
-        data: Matrix | None = None,
-        threshold_upper: float = 0.0,
-        threshold_lower: float = 0.0,
-        threshold_cutoff: float = math.inf,
-        spline: str = 'linear',
-        mirror: bool = False,
-        *,
-        env: Environment,
-        **kwargs: Any
-    ) -> None:
-        """Initialize."""
-
-        super().__init__(data, threshold_upper, threshold_lower, threshold_cutoff, spline, mirror, env=env, **kwargs)
-
-    def calc_achromatic_response(  # type: ignore[override]
-        self,
-        parameters: list[tuple[int, int, int, float]],
-        *,
-        env: Environment,
-        **kwargs: Any
-    ) -> None:  # pragma: no cover
-        """Precalculate the achromatic response."""
-
-        super().calc_achromatic_response(parameters, env=env, **kwargs)
-
-    def convert(self, coords: Vector, *, env: Environment, **kwargs: Any) -> Vector:  # type: ignore[override]
-        """Convert to the target color space."""
-
-        return xyz_d65_to_cam16_jmh(lin_srgb_to_xyz(lin_srgb(coords)), env)
-
 
 def hue_quadrature(h: float) -> float:
     """
@@ -614,59 +356,12 @@ class CAM16JMh(LChish, Space):
     WHITE = WHITES['2deg']['D65']
     # Assuming sRGB which has a lux of 64
     ENV = Environment(WHITE, 64 / math.pi * 0.2, 20, 'average', False)
-    # If discounting were True, we could remove the dynamic achromatic response
-    ACHROMATIC = Achromatic(
-        # Precalculated from:
-        # [
-        #     (1, 5, 1, 1000.0),
-        #     (1, 10, 1, 200.0),
-        #     (5, 521, 5, 100.0)
-        # ],
-        ACHROMATIC_RESPONSE,
-        0.0012,
-        0.0141,
-        6.7,
-        'catrom',
-        env=ENV
-    )
-    INV_ACHROMATIC = Achromatic(
-        # Precalculated from:
-        # [
-        #     [1, 5, 10, 1000],
-        #     [1, 511, 6, 100]
-        # ],
-        INV_ACHROMATIC_RESPONSE,
-        0.0295,
-        0.01508,
-        4.8,
-        'catrom',
-        env=ENV,
-        negative=True
-    )
     CHANNELS = (
         Channel("j", 0.0, 100.0),
         Channel("m", 0, 105.0),
-        Channel("h", 0.0, 360.0, flags=FLG_ANGLE, nans=ACHROMATIC.hue)
+        Channel("h", 0.0, 360.0, flags=FLG_ANGLE)
     )
 
-    def resolve_channel(self, index: int, coords: Vector) -> float:
-        """Resolve channels."""
-
-        # Assume black if J is below zero
-        J = coords[0]
-        achromatic = self.ACHROMATIC if J >= 0 else self.INV_ACHROMATIC
-
-        if index == 2:
-            h = coords[2]
-            return achromatic.get_ideal_hue(J, coords[1]) if math.isnan(h) else h
-
-        elif index == 1:
-            c = coords[1]
-            return achromatic.get_ideal_chroma(J) if math.isnan(c) else c
-
-        value = coords[index]
-        return self.channels[index].nans if math.isnan(value) else value
-
     def normalize(self, coords: Vector) -> Vector:
         """Normalize."""
 
@@ -675,16 +370,11 @@ def normalize(self, coords: Vector) -> Vector:
         coords[2] %= 360.0
         return coords
 
-    def is_achromatic(self, coords: Vector) -> bool:
+    def is_achromatic(self, coords: Vector) -> bool | None:
         """Check if color is achromatic."""
 
-        if coords[0] == 0:
-            return True
-
-        if coords[0] < 0:
-            return self.INV_ACHROMATIC.test(*self.normalize(coords))
-
-        return self.ACHROMATIC.test(*self.normalize(coords))
+        # Account for both positive and negative chroma
+        return coords[0] == 0 or abs(coords[1]) < ACHROMATIC_THRESHOLD
 
     def to_base(self, coords: Vector) -> Vector:
         """From CAM16 JMh to XYZ."""
diff --git a/coloraide/spaces/cam16_ucs.py b/coloraide/spaces/cam16_ucs.py
index 234f19be..d5830859 100644
--- a/coloraide/spaces/cam16_ucs.py
+++ b/coloraide/spaces/cam16_ucs.py
@@ -9,6 +9,7 @@
 import math
 from .cam16_jmh import CAM16JMh, xyz_d65_to_cam16, cam16_to_xyz_d65, Environment
 from ..spaces import Space, Labish
+from .lch import ACHROMATIC_THRESHOLD
 from ..cat import WHITES
 from .. import util
 from ..channels import Channel, FLG_MIRROR_PERCENT
@@ -101,38 +102,12 @@ class CAM16UCS(Labish, Space):
     WHITE = WHITES['2deg']['D65']
     # Use the same environment as CAM16JMh
     ENV = CAM16JMh.ENV
-    ACHROMATIC = CAM16JMh.ACHROMATIC
-    INV_ACHROMATIC = CAM16JMh.INV_ACHROMATIC
-
-    def resolve_channel(self, index: int, coords: Vector) -> float:
-        """Resolve channels."""
-
-        if index in (1, 2):
-            # Assume black if J is below zero
-            J = coords[0]
-
-            achromatic = self.INV_ACHROMATIC if J < 0 else self.ACHROMATIC
-
-            if not math.isnan(coords[index]):
-                return coords[index]
-
-            return achromatic.get_ideal_ab(J)[index - 1]
-
-        value = coords[index]
-        return self.channels[index].nans if math.isnan(value) else value
 
     def is_achromatic(self, coords: Vector) -> bool:
         """Check if color is achromatic."""
 
-        j, m, h = cam16_ucs_to_cam16_jmh(coords, self.MODEL)
-
-        if j == 0.0:
-            return True
-
-        if j < 0.0:
-            return self.INV_ACHROMATIC.test(j, m, h)
-
-        return self.ACHROMATIC.test(j, m, h)
+        j, m = cam16_ucs_to_cam16_jmh(coords, self.MODEL)[:-1]
+        return j == 0 or abs(m) < ACHROMATIC_THRESHOLD
 
     def to_base(self, coords: Vector) -> Vector:
         """To CAM16 JMh from CAM16."""
diff --git a/coloraide/spaces/hct.py b/coloraide/spaces/hct.py
index 868cd411..380c1fa1 100644
--- a/coloraide/spaces/hct.py
+++ b/coloraide/spaces/hct.py
@@ -47,168 +47,11 @@
 from ..cat import WHITES
 from ..channels import Channel, FLG_ANGLE
 from .cam16_jmh import Environment, cam16_to_xyz_d65, xyz_d65_to_cam16
-from .cam16_jmh import Achromatic as _Achromatic
-from .srgb_linear import lin_srgb_to_xyz
-from .srgb import lin_srgb
 from .lab import EPSILON, KAPPA, KE
-from ..types import Vector, Matrix  # noqa: F401
-from typing import Any
+from .lch import ACHROMATIC_THRESHOLD
+from ..types import Vector
 import math
 
-ACHROMATIC_RESPONSE = [
-    [0.06991457401674239, 0.14894004649491985, 209.54685746644427],
-    [0.13982914803348123, 0.19600973408107872, 209.54683151375164],
-    [0.20974372205022362, 0.2284758840823509, 209.54681244250116],
-    [0.279658296066966, 0.2539510473857674, 209.54679680373425],
-    [0.34957287008370486, 0.27520353123076896, 209.54678330540025],
-    [0.6991457401674133, 0.3503388633000774, 209.54673233806204],
-    [1.0487186102511181, 0.4013845937276958, 209.54669489478562],
-    [1.398291480334823, 0.44114839784490845, 209.54666419686697],
-    [1.7478643504185314, 0.474176553523894, 209.5466377050236],
-    [2.0974372205022362, 0.5026634742613169, 209.54661414586045],
-    [2.447010090585941, 0.5278524887931191, 209.5465927758291],
-    [2.7965829606696495, 0.5505226723840896, 209.5465731172251],
-    [3.1624547958278697, 0.5721193279495548, 209.54655401960312],
-    [3.5553195764898433, 0.5933528830664249, 209.5465348955287],
-    [3.9752712774319967, 0.6142212187429537, 209.5465157686296],
-    [4.422818762249047, 0.6347462660662626, 209.54649663922487],
-    [4.89845719045681, 0.6549477974516251, 209.54647750762507],
-    [5.40266895987223, 0.6748437111130419, 209.54645837410766],
-    [5.93592454797767, 0.6944502696114525, 209.54643923893252],
-    [6.4986832666748775, 0.7137823011126716, 209.5464201023271],
-    [7.091393942308237, 0.732853370157594, 209.54640096452033],
-    [7.714495530830426, 0.7516759233486977, 209.54638182570906],
-    [8.362902589291522, 0.7702614142702398, 209.54636268608706],
-    [9.010442756551821, 0.7886204111203621, 209.54634354582225],
-    [9.653817900540862, 0.8067626898669605, 209.5463244050959],
-    [10.29318377392433, 0.8246973152252656, 209.5463052640459],
-    [10.928685772528809, 0.8424327113314977, 209.5462861228275],
-    [11.56045990779819, 0.8599767236656302, 209.54626698157904],
-    [12.188633662809302, 0.8773366735026917, 209.5462478404262],
-    [12.813326748632115, 0.8945194059620861, 209.54622869948886],
-    [13.434651775012771, 0.9115313325460387, 209.54620955888788],
-    [14.0527148470809, 0.9283784689195937, 209.54619041872914],
-    [14.667616097925809, 0.9450664685622908, 209.5461712791161],
-    [15.279450165363095, 0.9616006528304082, 209.54615214014845],
-    [15.888306619956744, 0.9779860378836129, 209.54613300191994],
-    [16.494270350320917, 0.9942273588675171, 209.54611386451774],
-    [17.097421910858294, 1.0103290916839778, 209.5460947280279],
-    [17.697837836366574, 1.026295472637739, 209.5460755925407],
-    [18.295590927334914, 1.0421305162064542, 209.54605645811773],
-    [18.890750509238096, 1.057838031146838, 209.54603732484748],
-    [19.483382668700436, 1.0734216351263122, 209.54601819278727],
-    [20.073550469030984, 1.088884768038463, 209.54599906201673],
-    [20.661314147315657, 1.1042307041468111, 209.54597993259077],
-    [21.24673129498138, 1.1194625631780193, 209.54596080458563],
-    [21.829857023514606, 1.1345833204761497, 209.54594167805195],
-    [22.41074411681668, 1.1495958163105326, 209.54592255304232],
-    [22.989443171504917, 1.164502764423835, 209.54590342962592],
-    [23.566002726318594, 1.1793067598944942, 209.5458843078509],
-    [24.140469381658278, 1.1940102863793578, 209.54586518776134],
-    [24.71288791017345, 1.208615722794075, 209.54584606941722],
-    [25.283301359213652, 1.2231253494868157, 209.54582695285913],
-    [25.851751145872043, 1.2375413539480247, 209.5458078381372],
-    [26.418277145272675, 1.2518658361006216, 209.5457887252938],
-    [32.53317505979514, 1.4039207501018756, 209.5455786194677],
-    [58.346464585181536, 2.0040978388926978, 209.54462755393828],
-    [82.04578167434552, 2.5102950876783927, 209.54368432153106],
-    [104.37764532041983, 2.957132646110733, 209.54274979150162],
-    [125.71227094822433, 3.3610704881488145, 209.54182435083504],
-    [146.26918350453806, 3.731633656682073, 209.5409081799222],
-    [166.19277583771807, 4.074995088257203, 209.54000135154777],
-    [185.58476265482338, 4.395465139909588, 209.53910387571688],
-    [204.52038939834455, 4.696217703676614, 209.53821572300697],
-    [223.05740651867518, 4.979684699789179, 209.5373368379916],
-    [241.2414278875509, 5.247788076344199, 209.53646714753637],
-    [259.10932108008694, 5.502084789195138, 209.53560656613365],
-    [276.69145334478924, 5.743861871175241, 209.53475499956414],
-    [294.01323625159637, 5.9742012642698965, 209.53391234742955],
-    [311.09622156929686, 6.194025494618159, 209.5330785049965],
-    [327.9588994483437, 6.404130748156792, 209.5322533645655],
-    [344.61729300144975, 6.60521139322382, 209.53143681641436],
-    [361.08540993083295, 6.797878536697256, 209.53062874966722],
-    [377.375591468867, 6.9826743184757625, 209.5298290527965],
-    [393.4987860653063, 7.160083098082503, 209.52903761414294],
-    [409.4647669400027, 7.330540332852221, 209.52825432223364]
-]  # type: Matrix
-
-INV_ACHROMATIC_RESPONSE = [
-    [-44570.454917014395, 4.984058427842426, 29.528254322233643],
-    [-39738.74229269304, 4.868992098048034, 29.529037614142972],
-    [-35227.22948703891, 4.749243790113306, 29.5298290527965],
-    [-31029.54799495578, 4.624521731030067, 29.530628749667212],
-    [-27139.129218104852, 4.494501608270974, 29.53143681641436],
-    [-23549.186758448897, 4.358820705994274, 29.53225336456548],
-    [-20252.696013957233, 4.217070528241072, 29.533078504996485],
-    [-17242.37047122224, 4.068787389324386, 29.533912347429574],
-    [-14510.633904472961, 3.9134402232209555, 29.534754999564143],
-    [-12049.587431138627, 3.7504145097188952, 29.535606566133662],
-    [-9850.9700030737, 3.57899065066929, 29.53646714753636],
-    [-7906.1103682456205, 3.3983141993252333, 29.537336837991614],
-    [-6205.867715825722, 3.2073537531209646, 29.53821572300699],
-    [-4740.556934184132, 3.004839469667562, 29.539103875716876],
-    [-3499.8523254905363, 2.7891697871907946, 29.54000135154777],
-    [-2472.6600640203656, 2.5582631140416034, 29.540908179922234],
-    [-1646.9432511982377, 2.309307714994031, 29.54182435083504],
-    [-1009.4708758426449, 2.038306399455464, 29.542749791501617],
-    [-545.4349987904753, 1.7391570082516687, 29.543684321531057],
-    [-237.81389477171723, 1.4014938491794258, 29.54462755393829],
-    [-66.15647758957148, 1.0042248527096727, 29.545578619467726],
-    [-44.16877329851594, 0.9045142761589678, 29.54578872529381],
-    [-42.42258854100067, 0.8951499834879552, 29.54580783813722],
-    [-40.71735152221153, 0.885731507008267, 29.545826952859137],
-    [-39.05278319774536, 0.8762577488471229, 29.545846069417227],
-    [-37.428601621130795, 0.8667275715376151, 29.54586518776134],
-    [-35.84452186191744, 0.8571397960298269, 29.545884307850905],
-    [-34.300255919939744, 0.8474931995804906, 29.545903429625927],
-    [-32.795512635504856, 0.8377865135106229, 29.545922553042335],
-    [-31.329997595230186, 0.8280184208260168, 29.54594167805195],
-    [-29.90341303323391, 0.8181875536890815, 29.545960804585643],
-    [-28.51545772735428, 0.8082924907335349, 29.54597993259078],
-    [-27.165826890045, 0.7983317542132449, 29.54599906201674],
-    [-25.854212053560502, 0.7883038069759454, 29.546018192787304],
-    [-24.580300949008887, 0.7782070492505507, 29.54603732484749],
-    [-23.343777378809595, 0.7680398152420924, 29.54605645811775],
-    [-22.144321082046854, 0.7578003695232353, 29.5460755925407],
-    [-20.981607592159143, 0.7474869032182557, 29.54609472802791],
-    [-19.855308086347, 0.7370975299714788, 29.54611386451775],
-    [-18.76508922601634, 0.7266302817017825, 29.54613300191993],
-    [-17.710612987501253, 0.7160831041421349, 29.546152140148457],
-    [-16.69153648222621, 0.7054538521739349, 29.546171279116077],
-    [-15.70751176537303, 0.6947402849727958, 29.546190418729164],
-    [-14.75818563200937, 0.6839400609923351, 29.54620955888788],
-    [-13.843199399511406, 0.6730507328308233, 29.546228699488893],
-    [-12.962188674970657, 0.6620697420440577, 29.5462478404262],
-    [-12.11478310611083, 0.6509944140004065, 29.546266981579034],
-    [-11.300606114050115, 0.6398219529102014, 29.54628612282749],
-    [-10.519274606023421, 0.6285494372210045, 29.546305264045923],
-    [-9.770398665921691, 0.6171738156416642, 29.546324405095888],
-    [-9.053581220203, 0.6056919041652574, 29.54634354582226],
-    [-8.36841767637534, 0.5941003846022807, 29.546362686087097],
-    [-7.714495530830426, 0.5823958053405168, 29.54638182570907],
-    [-7.091393942308239, 0.5705745853224489, 29.54640096452036],
-    [-6.4986832666748775, 0.5586330226371172, 29.5464201023271],
-    [-5.9359245479776686, 0.5465673096808428, 29.546439238932532],
-    [-5.402668959872228, 0.5343735576616648, 29.546458374107672],
-    [-4.89845719045681, 0.5220478343999755, 29.546477507625063],
-    [-4.422818762249047, 0.5095862211204446, 29.546496639224877],
-    [-3.9752712774319967, 0.49698489651743805, 29.546515768629586],
-    [-3.555319576489845, 0.4842402602948852, 29.546534895528715],
-    [-3.1624547958278697, 0.4713491143947718, 29.54655401960312],
-    [-2.7965829606696477, 0.45832488304806807, 29.546573117225094],
-    [-2.447010090585943, 0.4447586387623133, 29.546592775829115],
-    [-2.0974372205022362, 0.42982773750036296, 29.546614145860463],
-    [-1.7478643504185296, 0.4131501637871255, 29.54663770502361],
-    [-1.398291480334823, 0.39414263959389384, 29.546664196866974],
-    [-1.04871861025112, 0.37184420794844997, 29.546694894785634],
-    [-0.6991457401674115, 0.3445030170602825, 29.546732338062064],
-    [-0.3495728700837084, 0.30872069573903993, 29.54678330540024],
-    [-0.27965829606696424, 0.3002727676037857, 29.546796803734274],
-    [-0.20974372205022362, 0.291838255404801, 29.54681244250118],
-    [-0.13982914803348123, 0.28544551295072706, 29.546831513751652],
-    [-0.06991457401674062, 0.2970033564541394, 29.546857466444255]
-]  # type: Matrix
-
 
 def y_to_lstar(y: float) -> float:
     """Convert XYZ Y to Lab L*."""
@@ -303,20 +146,6 @@ def xyz_to_hct(coords: Vector, env: Environment) -> Vector:
     return [h, c, t]
 
 
-class Achromatic(_Achromatic):
-    """Test HCT achromatic response."""
-
-    # Lightness and chroma (equivalent) index.
-    L_IDX = 2
-    C_IDX = 1
-    H_IDX = 0
-
-    def convert(self, coords: Vector, *, env: Environment, **kwargs: Any) -> Vector:  # type: ignore[override]
-        """Convert to the target color space."""
-
-        return xyz_to_hct(lin_srgb_to_xyz(lin_srgb(coords)), env)
-
-
 class HCT(LChish, Space):
     """HCT class."""
 
@@ -338,63 +167,12 @@ class HCT(LChish, Space):
         "hue": "h"
     }
 
-    # Achromatic detection
-    # Precalculated from:
-    # [
-    #     (1, 5, 1, 1000.0),
-    #     (1, 50, 1, 200.0),
-    #     (30, 551, 25, 100.0)
-    # ]
-    ACHROMATIC = Achromatic(
-        ACHROMATIC_RESPONSE,
-        0.0053,
-        0.0395,
-        8.1,
-        'catrom',
-        env=ENV,
-    )
-
-    # Precalculated from:
-    # [
-    #     (1, 5, 1, 1000.0),
-    #     (1, 50, 1, 200.0),
-    #     (30, 551, 25, 100.0)
-    # ]
-    INV_ACHROMATIC = Achromatic(
-        INV_ACHROMATIC_RESPONSE,
-        0.0049,
-        0.0646,
-        5.8,
-        'catrom',
-        env=ENV,
-        negative=True
-    )
-
-
     CHANNELS = (
         Channel("h", 0.0, 360.0, flags=FLG_ANGLE),
         Channel("c", 0.0, 145.0),
         Channel("t", 0.0, 100.0)
     )
 
-    def resolve_channel(self, index: int, coords: Vector) -> float:
-        """Resolve channels."""
-
-        # Treat tone less than zero as black
-        t = coords[2]
-        achromatic = self.INV_ACHROMATIC if t < 0 else self.ACHROMATIC
-
-        if index == 0:
-            h = coords[0]
-            return achromatic.get_ideal_hue(t, coords[1]) if math.isnan(h) else h
-
-        elif index == 1:
-            c = coords[1]
-            return achromatic.get_ideal_chroma(t) if math.isnan(c) else c
-
-        value = coords[index]
-        return self.channels[index].nans if math.isnan(value) else value
-
     def normalize(self, coords: Vector) -> Vector:
         """Normalize."""
 
@@ -403,18 +181,11 @@ def normalize(self, coords: Vector) -> Vector:
         coords[0] %= 360.0
         return coords
 
-    def is_achromatic(self, coords: Vector) -> bool:
+    def is_achromatic(self, coords: Vector) -> bool | None:
         """Check if color is achromatic."""
 
-        if coords[2] == 0:
-            return True
-
-        coords = self.normalize(coords)
-
-        if coords[2] < 0:
-            return self.INV_ACHROMATIC.test(coords[2], coords[1], coords[0])
-
-        return self.ACHROMATIC.test(coords[2], coords[1], coords[0])
+        # Account for both positive and negative chroma
+        return coords[2] == 0 or abs(coords[1]) < ACHROMATIC_THRESHOLD
 
     def names(self) -> tuple[str, ...]:
         """Return LCh-ish names in the order L C h."""
diff --git a/coloraide/spaces/igpgtg.py b/coloraide/spaces/igpgtg.py
index fc4455f3..4b08dd68 100644
--- a/coloraide/spaces/igpgtg.py
+++ b/coloraide/spaces/igpgtg.py
@@ -8,12 +8,7 @@
 from ..channels import Channel, FLG_MIRROR_PERCENT
 from ..cat import WHITES
 from .. import algebra as alg
-from .achromatic import Achromatic as _Achromatic
-from .srgb_linear import lin_srgb_to_xyz
-from .srgb import lin_srgb
-from ..types import Vector, Matrix  # noqa: F401
-from typing import Any
-import math
+from ..types import Vector
 
 XYZ_TO_LMS = [
     [2.968, 2.741, -0.649],
@@ -39,15 +34,6 @@
     [0.02265698651657832, -0.004701151874826367, -0.030048158824914562]
 ]
 
-ACHROMATIC_RESPONSE = [
-    [0.01710472400677632, 7.497407788263645e-05, 289.0071727628954],
-    [0.022996189520032607, 0.00010079777395973735, 289.00717276289754],
-    [0.027343043084773422, 0.00011985106810105086, 289.00717276288543],
-    [0.03091688192289772, 0.00013551605464416815, 289.0071727629022],
-    [0.9741484960046702, 0.004269924798539192, 289.00717276289504],
-    [5.049390603804086, 0.022132681254572965, 289.0071727628912]
-]  # type: Matrix
-
 
 def xyz_to_igpgtg(xyz: Vector) -> Vector:
     """XYZ to IgPgTg."""
@@ -73,18 +59,6 @@ def igpgtg_to_xyz(itp: Vector) -> Vector:
     return alg.matmul(LMS_TO_XYZ, lms_in, dims=alg.D2_D1)
 
 
-class Achromatic(_Achromatic):
-    """Test if color is achromatic."""
-
-    def convert(self, coords: Vector, **kwargs: Any) -> Vector:
-        """Convert to the target color space."""
-
-        lab = xyz_to_igpgtg(lin_srgb_to_xyz(lin_srgb(coords)))
-        l = lab[0]
-        c, h = alg.rect_to_polar(*lab[1:])
-        return [l, c, h]
-
-
 class IgPgTg(IPT):
     """The IgPgTg class."""
 
@@ -102,32 +76,6 @@ class IgPgTg(IPT):
         "tritan": "tg"
     }
     WHITE = WHITES['2deg']['D65']
-    # Precalculated from:
-    # [
-    #     (1, 5, 1, 1000.0),
-    #     (100, 101, 1, 100),
-    #     (520, 521, 1, 100)
-    # ]
-    ACHROMATIC = Achromatic(
-        ACHROMATIC_RESPONSE,
-        1e-5,
-        1e-5,
-        0.03126,
-        'linear',
-        mirror=True
-    )
-
-    def resolve_channel(self, index: int, coords: Vector) -> float:
-        """Resolve channels."""
-
-        if index in (1, 2):
-            if not math.isnan(coords[index]):
-                return coords[index]
-
-            return self.ACHROMATIC.get_ideal_ab(coords[0])[index - 1]
-
-        value = coords[index]
-        return self.channels[index].nans if math.isnan(value) else value
 
     def to_base(self, coords: Vector) -> Vector:
         """To XYZ."""
diff --git a/coloraide/spaces/ipt.py b/coloraide/spaces/ipt.py
index 547be8b2..217f4818 100644
--- a/coloraide/spaces/ipt.py
+++ b/coloraide/spaces/ipt.py
@@ -5,15 +5,10 @@
 221677980_Development_and_Testing_of_a_Color_Space_IPT_with_Improved_Hue_Uniformity.
 """
 from __future__ import annotations
-from ..spaces import Space, Labish
+from .lab import Lab
 from ..channels import Channel, FLG_MIRROR_PERCENT
 from .. import algebra as alg
-from .achromatic import Achromatic as _Achromatic
-from .srgb_linear import lin_srgb_to_xyz
-from .srgb import lin_srgb
-from ..types import Vector, Matrix  # noqa: F401
-from typing import Any
-import math
+from ..types import Vector
 from .. import util
 
 XYZ_TO_LMS = [
@@ -40,15 +35,6 @@
     [1.0, 0.03261510991706641, -0.6768871830691794]
 ]
 
-ACHROMATIC_RESPONSE = [
-    [0.017066845239980113, 1.3218447776798768e-06, 329.76026731824635],
-    [0.022993026958471587, 1.7808336678784566e-06, 329.76026731797435],
-    [0.02737255832988907, 2.1200328924793134e-06, 329.7602673179663],
-    [0.03097697795223092, 2.3991989122003048e-06, 329.7602673180789],
-    [0.9999910919149724, 7.745034210925492e-05, 329.7602673179579],
-    [5.243613106559707, 0.0004061232467760781, 329.76026731814255]
-]  # type: Matrix
-
 
 def xyz_to_ipt(xyz: Vector) -> Vector:
     """XYZ to IPT."""
@@ -64,19 +50,7 @@ def ipt_to_xyz(ipt: Vector) -> Vector:
     return alg.matmul(LMS_TO_XYZ, lms, dims=alg.D2_D1)
 
 
-class Achromatic(_Achromatic):
-    """Test achromatic response."""
-
-    def convert(self, coords: Vector, **kwargs: Any) -> Vector:
-        """Convert to the target color space."""
-
-        lab = xyz_to_ipt(lin_srgb_to_xyz(lin_srgb(coords)))
-        l = lab[0]
-        c, h = alg.rect_to_polar(*lab[1:])
-        return [l, c, h]
-
-
-class IPT(Labish, Space):
+class IPT(Lab):
     """The IPT class."""
 
     BASE = "xyz-d65"
@@ -97,38 +71,6 @@ class IPT(Labish, Space):
     # We use chromaticity points (0.31270, 0.3290) which gives us an XYZ of ~[0.9505, 1.0000, 1.0890]
     # IPT uses XYZ of [0.9504, 1.0, 1.0889] which yields chromaticity points ~(0.3127035830618893, 0.32902313032606195)
     WHITE = tuple(util.xyz_to_xyY([0.9504, 1.0, 1.0889])[:-1])  # type: ignore[assignment]
-    # Precalculated from:
-    # [
-    #     (1, 5, 1, 1000.0),
-    #     (100, 101, 1, 100),
-    #     (520, 521, 1, 100)
-    # ]
-    ACHROMATIC = Achromatic(
-        ACHROMATIC_RESPONSE,
-        1e-5,
-        1e-5,
-        0.00049,
-        'linear',
-        mirror=True
-    )  # type: _Achromatic
-
-    def resolve_channel(self, index: int, coords: Vector) -> float:
-        """Resolve channels."""
-
-        if index in (1, 2):
-            if not math.isnan(coords[index]):
-                return coords[index]
-
-            return self.ACHROMATIC.get_ideal_ab(coords[0])[index - 1]
-
-        value = coords[index]
-        return self.channels[index].nans if math.isnan(value) else value
-
-    def is_achromatic(self, coords: Vector) -> bool:
-        """Check if color is achromatic."""
-
-        m, h = alg.rect_to_polar(coords[1], coords[2])
-        return self.ACHROMATIC.test(coords[0], m, h)
 
     def to_base(self, coords: Vector) -> Vector:
         """To XYZ."""
diff --git a/coloraide/spaces/jzazbz.py b/coloraide/spaces/jzazbz.py
index 36fa5371..e0b483d1 100644
--- a/coloraide/spaces/jzazbz.py
+++ b/coloraide/spaces/jzazbz.py
@@ -20,17 +20,12 @@
 """
 from __future__ import annotations
 from ..spaces import Space, Labish
-from .achromatic import Achromatic as _Achromatic
 from ..cat import WHITES
 from ..channels import Channel, FLG_MIRROR_PERCENT
 from .. import util
 from .. import algebra as alg
-from .lch import lab_to_lch
-from .srgb_linear import lin_srgb_to_xyz
-from .srgb import lin_srgb
-from ..types import Vector, Matrix  # noqa: F401
-from typing import Any
-import math
+from ..types import Vector  # noqa: F401
+from .lch import ACHROMATIC_THRESHOLD
 
 B = 1.15
 G = 0.66
@@ -78,107 +73,6 @@
     [1.0, -0.09601924202631895, -0.811891896056039]
 ]
 
-ACHROMATIC_RESPONSE = [
-    [0.0009185262133445958, 2.840443191466584e-06, 216.0885802021336],
-    [0.0032449260086909186, 8.875176418290735e-06, 216.0865538095895],
-    [0.007045724283550615, 1.742628198531671e-05, 216.08513073729978],
-    [0.009865500056099596, 2.317265885648471e-05, 216.08447255685712],
-    [0.012214842923826377, 2.768276363465363e-05, 216.0840428784794],
-    [0.014313828964294468, 3.15364404794859e-05, 216.08371809158197],
-    [0.016438077905865333, 3.5290049847676045e-05, 216.08343078937077],
-    [0.018605469297130712, 3.898508192957335e-05, 216.08317068540933],
-    [0.02080811172991238, 4.261478385537034e-05, 216.0829334034566],
-    [0.02303956968571666, 4.617490464976015e-05, 216.0827155404453],
-    [0.02529454916234039, 4.966297634911679e-05, 216.0825143871757],
-    [0.027568661881969103, 5.307781011104134e-05, 216.08232775391835],
-    [0.029858245862958505, 5.641913782115498e-05, 216.08215383175184],
-    [0.032160227098370596, 5.9687353969353895e-05, 216.0819911399305],
-    [0.03447201166859765, 6.288332788976944e-05, 216.08183842876093],
-    [0.03679140069552315, 6.600826589362147e-05, 216.08169464885202],
-    [0.03911652265347357, 6.906360946484213e-05, 216.0815588821907],
-    [0.04144577901905777, 7.20509595041043e-05, 216.08143037308045],
-    [0.04377780027866024, 7.497201997536904e-05, 216.0813084440367],
-    [0.046111410055380574, 7.78285557456021e-05, 216.08119250961198],
-    [0.04844559565682705, 8.062236105672717e-05, 216.08108206116256],
-    [0.050779483741710464, 8.335523609797642e-05, 216.08097664284261],
-    [0.053112320097915965, 8.602896956101964e-05, 216.0808758658446],
-    [0.05544345274603036, 8.864532585124454e-05, 216.08077937053],
-    [0.05777231775004377, 9.120603576758393e-05, 216.08068683882303],
-    [0.060098427245276025, 9.371278988327557e-05, 216.08059799668482],
-    [0.062421359292580254, 9.616723398809035e-05, 216.08051257202447],
-    [0.07282655496930118, 0.00010660656866708172, 216.08016488545363],
-    [0.09108704537148082, 0.00012302746725157782, 216.07966036150265],
-    [0.10900798276245852, 0.00013720664366179703, 216.0792568965177],
-    [0.12658714074774824, 0.00014958895848933898, 216.0789232434426],
-    [0.1438408486329413, 0.00016050730576011498, 216.07864044077417],
-    [0.16079190286441, 0.00017021501266814123, 216.07839616904852],
-    [0.17746453522326563, 0.0001789083001012923, 216.07818199543323],
-    [0.1938822908546569, 0.00018674170523656867, 216.07799190659145],
-    [0.21006717973109132, 0.00019383878540645526, 216.07782150011536],
-    [0.22603940395038172, 0.00020029967178728176, 216.0776674210859],
-    [0.24181734649813305, 0.0002062064957239789, 216.0775271074707],
-    [0.25741767436011453, 0.00021162735571841754, 216.0773985097046],
-    [0.27285548569428925, 0.00021661926443777217, 216.07728001861],
-    [0.28814446743495276, 0.00022123037188011426, 216.07717029782012],
-    [0.3032970476681226, 0.00022550166532677018, 216.07706827050953],
-    [0.318324536052124, 0.00022946828610769828, 216.07697303732877],
-    [0.3332372500029604, 0.00023316056105526304, 216.07688383235663],
-    [0.34804462653287077, 0.00023660481904069536, 216.0768000283236],
-    [0.3627553206321442, 0.00023982404450001245, 216.07672106548637],
-    [0.37737729148697624, 0.0002428384038912725, 216.07664646902424],
-    [0.3919178779258038, 0.0002456656744964799, 216.07657583096483],
-    [0.40638386443891256, 0.00024832159559521613, 216.07650879846213],
-    [0.420781539003173, 0.0002508201586917396, 216.07644505801895],
-    [0.4351167438075533, 0.0002531738482749366, 216.07638435201847],
-    [0.44939491983780117, 0.0002553938438855013, 216.0763264167306],
-    [0.46362114614988975, 0.00025749018901337296, 216.076271046612],
-    [0.47780017454650436, 0.000259471935031054, 216.0762180267072],
-    [0.49193646026919147, 0.0002613472624003357, 216.07616722683406],
-    [0.506034189231372, 0.0002631235853618668, 216.07611845640926],
-    [0.520097302241697, 0.0002648076406577341, 216.07607158130352],
-    [0.5341295166035784, 0.0002664055645672449, 216.07602649958457],
-    [0.5481343454215033, 0.0002679229595196272, 216.07598307638068],
-    [0.562115114898706, 0.00026936495156998765, 216.0759412038042],
-    [0.5760749798710889, 0.00027073624083088496, 216.0759007944157],
-    [0.5900169377887273, 0.0002720411453401863, 216.07586174613544],
-    [0.6039438413278134, 0.00027328363966826354, 216.07582400851473],
-    [0.6178584097918066, 0.0002744673887323733, 216.07578748393507],
-    [0.6317632394388409, 0.0002755957777534978, 216.07575212814703],
-    [0.6456608128558685, 0.00027667193897750657, 216.07571787420144],
-    [0.6595535074843613, 0.0002776987749361305, 216.0756845900282],
-    [0.6734436033881548, 0.00027867897881155425, 216.0756523336041],
-    [0.6873332903451649, 0.0002796150541276385, 216.07562100643182],
-    [0.7012246743322099, 0.0002805093307679912, 216.07559056780278],
-    [0.7151197834661946, 0.00028136397973813873, 216.07556096861626],
-    [0.7290205734555001, 0.00028218102717968054, 216.07553214064387],
-    [0.7429289326111091, 0.00028296236534187003, 216.07550410795596],
-    [0.7568466864597538, 0.0002837097645372413, 216.07547681413567],
-    [0.7707756019973733, 0.00028442488261536087, 216.0754501913429],
-    [0.7847173916174169, 0.0002851092732967129, 216.07542424437477],
-    [0.7986737167436694, 0.0002857643945778034, 216.07539893412002],
-    [0.8126461911946142, 0.0002863916163923197, 216.07537422001636],
-    [0.82663638430444, 0.0002869922264017996, 216.07535011510663],
-    [0.8406458238212122, 0.00028756743686351684, 216.07532657427606],
-    [0.8546759986026592, 0.0002881183896480139, 216.07530355785332],
-    [0.8687283611263583, 0.00028864616127277436, 216.07528106686505],
-    [0.8828043298308796, 0.0002891517676790176, 216.0752590661328],
-    [0.8969052913010638, 0.0002896361684484102, 216.07523755309595],
-    [0.9110326023119082, 0.0002901002704615418, 216.07521649953736],
-    [0.9251875917408455, 0.0002905449316470965, 216.07519588476185],
-    [0.939371562360337, 0.00029097096413252364, 216.0751757112678],
-    [0.9535857925200474, 0.0002913791373231644, 216.07515593963097],
-    [0.9678315377267531, 0.000291770180656392, 216.0751365593372]
-]  # type: Matrix
-
-
-class Achromatic(_Achromatic):
-    """Test if color is achromatic."""
-
-    def convert(self, coords: Vector, **kwargs: Any) -> Vector:
-        """Convert to the target color space."""
-
-        return lab_to_lch(xyz_d65_to_jzazbz(lin_srgb_to_xyz(lin_srgb(coords))))
-
 
 def jzazbz_to_xyz_d65(jzazbz: Vector) -> Vector:
     """From Jzazbz to XYZ."""
@@ -244,43 +138,12 @@ class Jzazbz(Labish, Space):
     }
     WHITE = WHITES['2deg']['D65']
     DYNAMIC_RANGE = 'hdr'
-    # Precalculated from
-    # [
-    #     (1, 5, 5, 1000.0),
-    #     (1, 52, 2, 200.0),
-    #     (30, 521, 8, 100.0)
-    # ]
-    ACHROMATIC = Achromatic(
-        ACHROMATIC_RESPONSE,
-        2.121e-7,
-        9.863e-7,
-        0.00039,
-        'catrom',
-    )
-
-    def resolve_channel(self, index: int, coords: Vector) -> float:
-        """Resolve channels."""
-
-        if index in (1, 2):
-            jz = coords[0]
-            if jz < 0:
-                jz = 0.0
-            if not math.isnan(coords[index]):
-                return coords[index]
-
-            return self.ACHROMATIC.get_ideal_ab(jz)[index - 1]
-
-        value = coords[index]
-        return self.channels[index].nans if math.isnan(value) else value
 
     def is_achromatic(self, coords: Vector) -> bool:
         """Check if color is achromatic."""
 
-        if coords[0] < 0:
-            return True
-
-        c, h = alg.rect_to_polar(coords[1], coords[2])
-        return self.ACHROMATIC.test(coords[0], c, h)
+        # Account for both positive and negative chroma
+        return coords[0] < 0 or alg.rect_to_polar(coords[1], coords[2])[0] < ACHROMATIC_THRESHOLD
 
     def to_base(self, coords: Vector) -> Vector:
         """To XYZ from Jzazbz."""
diff --git a/coloraide/spaces/jzczhz.py b/coloraide/spaces/jzczhz.py
index cf5decc1..5c66a68c 100644
--- a/coloraide/spaces/jzczhz.py
+++ b/coloraide/spaces/jzczhz.py
@@ -4,10 +4,9 @@
 https://www.osapublishing.org/oe/fulltext.cfm?uri=oe-25-13-15131&id=368272
 """
 from __future__ import annotations
-import math
 from ..cat import WHITES
 from .lch import LCh
-from .jzazbz import Jzazbz
+from .jzazbz import ACHROMATIC_THRESHOLD  # type: ignore[attr-defined]
 from ..channels import Channel, FLG_ANGLE
 from ..types import Vector
 
@@ -32,37 +31,17 @@ class JzCzhz(LCh):
         'c': 'cz',
         'j': 'jz'
     }
-    ACHROMATIC = Jzazbz.ACHROMATIC
     CHANNELS = (
         Channel("jz", 0.0, 1.0),
         Channel("cz", 0.0, 1.0),
-        Channel("hz", 0.0, 360.0, flags=FLG_ANGLE, nans=ACHROMATIC.hue)
+        Channel("hz", 0.0, 360.0, flags=FLG_ANGLE)
     )
 
-    def resolve_channel(self, index: int, coords: Vector) -> float:
-        """Resolve channels."""
-
-        jz = coords[0]
-        if jz < 0:
-            jz = 0.0
-        if index == 2:
-            h = coords[2]
-            return self.ACHROMATIC.get_ideal_hue(jz, coords[1]) if math.isnan(h) else h
-
-        elif index == 1:
-            c = coords[1]
-            return self.ACHROMATIC.get_ideal_chroma(jz) if math.isnan(c) else c
-
-        value = coords[index]
-        return self.channels[index].nans if math.isnan(value) else value
-
     def is_achromatic(self, coords: Vector) -> bool:
         """Check if color is achromatic."""
 
-        if coords[0] < 0.0:
-            return True
-
-        return self.ACHROMATIC.test(*self.normalize(coords))
+        # Account for both positive and negative chroma
+        return coords[0] < 0 or abs(coords[1]) < ACHROMATIC_THRESHOLD
 
     def hue_name(self) -> str:
         """Hue name."""
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index e0d22146..abe0a240 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,5 +1,14 @@
 # Changelog
 
+## 3.2
+
+-   **NEW**: `cam16-jmh`, `cam16-ucs`, `jzczhz`, `jzazbz`, `ipt`, and `igpgtg` will perform achromatic handling relative
+    to themselves. As an example, in CAM16, when chroma is close to zero, color will be determined as achromatic. This
+    is due to adapting luminance and background luminance, zero chroma may not always be perfect white, but it is
+    achromatic according to the space. This simplifies logic, improves speed, and lessens confusion as to what
+    achromatic means.
+-   **NEW**: Remove deprecated CAM16 Jab implementation. Use `cam16-ucs` instead.
+
 ## 3.1.2
 
 -   **ENHANCE**: Further improvements to both speed and accuracy of ray trace gamut mapping.
diff --git a/docs/src/markdown/advanced.md b/docs/src/markdown/advanced.md
index 588376e0..19395eb9 100644
--- a/docs/src/markdown/advanced.md
+++ b/docs/src/markdown/advanced.md
@@ -179,43 +179,3 @@ Color('darkgray').convert('xyz-d65').convert('hsl')[:-1]
 This problem can exist in various scenarios in pretty much all cylindrical color spaces. Some have tighter algorithms
 and may give really good results with sRGB, but then when converting from some other color space we'll see maybe not as
 tight a translation to and from.
-
-Additionally, some color spaces have very dynamic achromatic responses, as an interesting example, let's consider CAM16
-JMh. This color space actually has its lower limit for achromatic colors gradually rise higher and higher as lightness
-increases. Not only that, the achromatic line actually passes mainly through hue ~209.5 for most achromatic colors
-lighter than black.
-
-```py play
-Color('color(srgb 0 0 0)').convert('cam16-jmh', norm=False)[:]
-Color('color(srgb 0.5 0.5 0.5)').convert('cam16-jmh', norm=False)[:]
-Color('color(srgb 1 1 1)').convert('cam16-jmh', norm=False)[:]
-```
-
-This can make it hard to specify a simple chroma check for achromatic colors. Simply lowering the chroma or changing the
-hue can make the color no longer achromatic.
-
-```py play
-white = Color('color(srgb 1 1 1)')
-white.convert('cam16-jmh', norm=False).convert('srgb').to_string(hex=True)
-white.convert('cam16-jmh', norm=False).set('h', 0).convert('srgb').to_string(hex=True)
-white.convert('cam16-jmh', norm=False).set('m', 0.0).convert('srgb').to_string(hex=True)
-```
-
-For these types of color spaces, ColorAide will map the achromatic response with a spline and use it as a reference
-to detect achromatic values for undefined chroma and hue.
-
-```py play
-Color('cam16-jmh', [100, NaN, NaN]).convert('srgb').to_string(hex=True)
-Color('cam16-jmh', [50, NaN, NaN]).convert('srgb').to_string(hex=True)
-Color('cam16-jmh', [20, NaN, NaN]).convert('srgb').to_string(hex=True)
-```
-
-Depending on how well we can fit the achromatic response, the better the accuracy, but we do purposely allow some wiggle
-room to ensure we can capture achromatic colors within the threshold of the spline's accuracy. This can introduce some
-loss of accuracy, but makes working with achromatic colors in difficult spaces like CAM16 JMh more reasonable.
-
-```py play
-gray = Color('cam16-jmh', [50, NaN, NaN])
-gray.normalize()
-Color.interpolate([gray, 'green'], space='cam16-jmh')
-```
diff --git a/docs/src/markdown/interpolation.md b/docs/src/markdown/interpolation.md
index 35ba057f..4bafb203 100644
--- a/docs/src/markdown/interpolation.md
+++ b/docs/src/markdown/interpolation.md
@@ -1057,7 +1057,7 @@ color.mix(color2, space="hsl")
 ```
 
 Technically, any channel can be set to `NaN`. And there are various ways to do this. The
-[Color Manipulation documentation](./manipulation.md#undefined-values) goes into the details of how these `Nan` values
+[Color Manipulation documentation](./manipulation.md#undefined-values) goes into the details of how these `NaN` values
 naturally occur and the various ways a user and manipulate them.
 
 ## Carrying-Forward
diff --git a/docs/src/markdown/manipulation.md b/docs/src/markdown/manipulation.md
index 793320fc..50a7fe7f 100644
--- a/docs/src/markdown/manipulation.md
+++ b/docs/src/markdown/manipulation.md
@@ -348,18 +348,8 @@ always be true.
     jmh.set('hue', 0).convert('srgb')
     ```
 
-    For color spaces with more dynamic achromatic response, ColorAide will resolve undefined hues and chroma with real
-    values that are neutral for that color's given lightness. This doesn't just apply to cylindrical spaces either.
-    This behavior can be seen in non cylindrical spaces as well, like the Lab form of CAM16.
-
-    ```py play
-    Color('gray').convert('cam16-ucs')
-    Color('cam16-ucs', [56.23, NaN, NaN]).normalize(nans=False)
-    ```
-
-    The selected values may not always perfectly precise, but they are much better than blindly assuming zero.
-
-    There are a number of spaces that benefit from this approach: Jzazbz/JzCzhz, IPT, IgPgTg.
+    Some color spaces, due to things like adapting luminance and background luminance, will have their own achromatic
+    response, meaning that achromatic colors are neutral, but may not be pure white, gray, etc.
 
 2.  Most of the time, if you set all color channels to undefined, when resolved, the color will be black (or white in
     the case of CMYK). Unfortunately, using `0` for undefined channels in some color spaces can create colors outside
diff --git a/tools/calc_cam16_min_m.py b/tools/calc_cam16_min_m.py
deleted file mode 100644
index ae73250a..00000000
--- a/tools/calc_cam16_min_m.py
+++ /dev/null
@@ -1,182 +0,0 @@
-"""
-Map our achromatic spline calculation against real achromatic response.
-
-Report the delta between our spline and the real world. Also note the highest chroma climbs.
-"""
-import sys
-import argparse
-import os
-import matplotlib.pyplot as plt
-import math
-
-sys.path.insert(0, os.getcwd())
-
-from coloraide.everything import ColorAll as Color  # noqa: E402
-from coloraide.spaces.cam16_jmh import Environment, xyz_d65_to_cam16, Achromatic  # noqa: E402
-from coloraide.cat import WHITES  # noqa: E402
-
-
-def main():
-    """Main."""
-
-    parser = argparse.ArgumentParser(
-        prog='calc_cam16_ucs_jmh_min_m.py',
-        description='Calculate min M for achromatic colors in CAM16 UCS JMh and map current spline against real values.'
-    )
-    # Flag arguments
-    parser.add_argument(
-        '--res', '-r', type=int, default=50000, help="Resolution to use when calculating range, default is 50000."
-    )
-    parser.add_argument(
-        '--white-point', '-w', type=str, default='2deg:D65', help="White point 'deg:wp', example: '2deg:D65'."
-    )
-    parser.add_argument(
-        '--adapting-luminance', '-a', type=float, default=64 / math.pi * 0.2, help="Adapting luminance."
-    )
-    parser.add_argument(
-        '--background-luminance', '-b', type=float, default=20, help="Background luminace - default 20 (gray world)."
-    )
-    parser.add_argument(
-        '--surround', '-s', type=str, default='average', help="Surround: 'average', 'dim', 'dark'"
-    )
-    parser.add_argument(
-        '--discounting', '-d', action='store_true', help="Enable discounting."
-    )
-    parser.add_argument(
-        '--spline', '-S', type=str, default='catrom', help="Spline to use for approximation of achromatic line"
-    )
-    parser.add_argument(
-        '--negative', '-n', action='store_true', help="Negative lightness spline."
-    )
-    parser.add_argument(
-        '--tuning', '-t', type=str, action='append',
-        help="Spline tuning parameters: start:end:step:scale (int:int:int:float)"
-    )
-    parser.add_argument(
-        '--dump', action='store_true', help="Dump calculated values."
-    )
-    args = parser.parse_args()
-
-    return run(
-        args.white_point,
-        args.adapting_luminance,
-        args.background_luminance,
-        args.surround,
-        args.discounting,
-        args.spline,
-        args.tuning,
-        args.res,
-        args.negative,
-        args.dump
-    )
-
-
-def run(white_point, la, ba, surround, discounting, spline, tuning, res, negative, dump):
-    """Run."""
-
-    deg, wp = white_point.split(':')
-    tune = [[int(i) if e < 3 else float(i) for e, i in enumerate(x.split(':'))] for x in tuning]
-
-    env = Environment(WHITES[deg][wp], la, ba, surround, discounting)
-    convert = xyz_d65_to_cam16
-
-    test = Achromatic(spline=spline, env=env, negative=negative)
-    test.calc_achromatic_response(tune, env=env)
-
-    color = Color('srgb', [0, 0, 0])
-    points1 = {}
-    points2 = {}
-    diff_over = 0
-    diff_under = 0
-    j_under = 0
-    j_over = 0
-    max_m = 0
-    first = False
-
-    for i in range(1, res + 1):
-        div = res / 5
-        v = i / div
-        if negative:
-            v = -v
-
-        if abs(v) < 0.001:
-            continue
-
-        color.update('srgb', [v, v, v])
-        xyz = color.convert('xyz-d65')
-        coords = convert(xyz[:-1], env)[:]
-        j, m, h = coords[0], coords[5], coords[2]
-        if not first:
-            print('Starting J: ', j)
-            print('Starting M: ', m)
-            first = True
-
-        if m > max_m:
-            max_m = m
-
-        if not env.discounting:
-            domain = test.scale(j)
-            calc = test.spline(domain)[:]
-        else:
-            calc = (j, 1e-08, 0)
-
-        delta = calc[1] - m
-        if delta >= 0 and delta > diff_over:
-            diff_over = delta
-            j_over = j
-        if delta < 0 and abs(delta) > diff_under:
-            diff_under = abs(delta)
-            j_under = j
-
-        points1[j] = (m, h)
-        points2[calc[0]] = (calc[1], calc[2])
-
-    print('Delta Over: ', diff_over, j_over)
-    print('Delta Under: ', diff_under, j_under)
-    print('Max M: ', max_m)
-    if not env.discounting:
-        print('Data Points: ', test.spline.length)
-
-    j1 = []
-    j2 = []
-    m1 = []
-    m2 = []
-    h1 = []
-    h2 = []
-    for j in sorted(points1):
-        j1.append(j)
-        m1.append(points1[j][0])
-        h1.append(points1[j][1])
-    for j in sorted(points2):
-        j2.append(j)
-        m2.append(points2[j][0])
-        h2.append(points2[j][1])
-
-    figure = plt.figure()
-
-    ax = plt.axes(
-        projection='3d',
-        xlabel='M',
-        ylabel='H',
-        zlabel='J'
-    )
-
-    # ax.set_aspect('auto')
-    ax.set_title('JMh: Delta = {} - Max M = {}'.format(max(diff_over, diff_under), max_m))
-    figure.add_axes(ax)
-
-    # Print the calculated line against the real line
-    plt.style.use('seaborn-v0_8-whitegrid')
-    plt.plot(m1, h1, j1, '.', color='black')
-    plt.plot(m2, h2, j2, '.', color='red', markersize=0.5)
-    plt.show()
-
-    if dump:
-        print('===== Data =====')
-        print(test.dump())
-
-    return 0
-
-
-if __name__ == "__main__":
-    sys.exit(main())
diff --git a/tools/calc_hct_min_c.py b/tools/calc_hct_min_c.py
deleted file mode 100644
index 651929d5..00000000
--- a/tools/calc_hct_min_c.py
+++ /dev/null
@@ -1,142 +0,0 @@
-"""
-Map our achromatic spline calculation against real achromatic response.
-
-Report the delta between our spline and the real world. Also note the highest chroma climbs.
-"""
-import sys
-import argparse
-import os
-import matplotlib.pyplot as plt
-
-sys.path.insert(0, os.getcwd())
-
-from coloraide.everything import ColorAll as Color  # noqa: E402
-from coloraide.spaces.hct import HCT, xyz_to_hct, Achromatic  # noqa: E402
-
-
-def main():
-    """Main."""
-
-    parser = argparse.ArgumentParser(
-        prog='calc_hct_min_chroma.py',
-        description='Calculate minimum chroma for achromatic colors in HCT and maps current spline against real values.'
-    )
-    # Flag arguments
-    parser.add_argument(
-        '--res', '-r', type=int, default=50000, help="Resolution to use when calculating range, default is 10000."
-    )
-    parser.add_argument(
-        '--spline', '-S', type=str, default='catrom', help="Spline to use for approximation of achromatic line"
-    )
-    parser.add_argument(
-        '--tuning', '-t', type=str, action='append',
-        help="Spline tuning parameters: start:end:step:scale (int:int:int:float)"
-    )
-    parser.add_argument(
-        '--negative', '-n', action='store_true', help="Negative lightness spline."
-    )
-    parser.add_argument(
-        '--dump', action='store_true', help="Dump calculated values."
-    )
-    args = parser.parse_args()
-
-    return run(args.spline, args.tuning, args.res, args.negative, args.dump)
-
-
-def run(spline, tuning, res, negative, dump):
-    """Run."""
-
-    tune = [[int(i) if e < 3 else float(i) for e, i in enumerate(x.split(':'))] for x in tuning]
-    env = HCT.ENV
-    test = Achromatic(spline=spline, env=env, negative=negative)
-    test.calc_achromatic_response(tune, env=env)
-
-    color = Color('srgb', [0, 0, 0])
-    points1 = {}
-    points2 = {}
-    diff_over = 0
-    diff_under = 0
-    max_m = 0
-    first = False
-
-    for i in range(res + 1):
-        div = res / 5
-        v = i / div
-        if negative:
-            v = -v
-
-        if abs(v) < 0.001:
-            continue
-
-        color.update('srgb', [v, v, v])
-        xyz = color.convert('xyz-d65')
-        h, c, t = xyz_to_hct(xyz[:-1], HCT.ENV)
-        if not first:
-            print('Starting T: ', t)
-            print('Starting C: ', c)
-            first = True
-
-        if c > max_m:
-            max_m = c
-
-        domain = test.scale(t)
-        calc = test.spline(domain)[:]
-
-        delta = calc[1] - c
-        if delta >= 0 and delta > diff_over:
-            diff_over = delta
-        if delta < 0 and abs(delta) > diff_under:
-            diff_under = abs(delta)
-
-        points1[t] = (c, h)
-        points2[calc[0]] = (calc[1], calc[2])
-
-    print('Delta Over: ', diff_over)
-    print('Delta Under: ', diff_under)
-    print('Max M: ', max_m)
-    print('Data Points: ', test.spline.length)
-
-    t1 = []
-    t2 = []
-    m1 = []
-    m2 = []
-    h1 = []
-    h2 = []
-    for t in sorted(points1):
-        t1.append(t)
-        m1.append(points1[t][0])
-        h1.append(points1[t][1])
-    for t in sorted(points2):
-        t2.append(t)
-        m2.append(points2[t][0])
-        h2.append(points2[t][1])
-
-    figure = plt.figure()
-
-    # Create axes
-    ax = plt.axes(
-        projection='3d',
-        xlabel='C',
-        ylabel='H',
-        zlabel='T'
-    )
-
-    ax.set_aspect('auto')
-    ax.set_title('HCT: Delta = {} - Max C = {}'.format(max(diff_over, diff_under), max_m))
-    figure.add_axes(ax)
-
-    # Print the calculated line against the real line
-    plt.style.use('seaborn-v0_8-whitegrid')
-    plt.plot(m1, h1, t1, '.', color='black')
-    plt.plot(m2, h2, t2, '.', color='red', markersize=0.5)
-    plt.show()
-
-    if dump:
-        print('===== Data =====')
-        print(test.dump())
-
-    return 0
-
-
-if __name__ == "__main__":
-    sys.exit(main())
diff --git a/tools/calc_lch_threshold.py b/tools/calc_lch_threshold.py
deleted file mode 100644
index 0bfea352..00000000
--- a/tools/calc_lch_threshold.py
+++ /dev/null
@@ -1,142 +0,0 @@
-"""
-Calculate the LCh threshold of LCh-ish colors.
-
-Particularly, we are interested in seeing the greatest
-chroma deviation from zero for achromatic colors.
-
-We simply will take a number of RGB color spaces and loop
-through all achromatic colors, significantly past white
-to see how far from zero chroma deviates.
-
-This is mainly due to the fact that many of these LCh-ish
-color spaces, as they get close to zero, whether due to
-the algorithm, the precision, floating point math, or
-whatever the reason, the conversion to an achromatic color
-in the LCh-ish space often is very, very close to zero, but
-not quite.
-
-The threshold is essentially used during conversion to round
-off chroma if it is very, very close to zero giving better,
-expected conversions.
-
-Looking at its Lab counterpart's `a` and `b` value is optional.
-This is mainly if you are curious to see how the other channels
-respond, or if you wanted use `a` and `b` to determine achromatic
-response.
-"""
-import sys
-import argparse
-import os
-import re
-
-sys.path.insert(0, os.getcwd())
-
-try:
-    from coloraide_extras.everything import ColorAll as Color
-except ImportError:
-    from coloraide.everything import ColorAll as Color
-
-RE_LEAD_ZERO = re.compile(r'^0\.0+')
-
-
-def run(lch, lab, verify):
-    """Run the calculation."""
-
-    max_chroma = 0.0
-    max_a = 0.0
-    max_b = 0.0
-
-    for space in ('srgb', 'display-p3', 'rec2020', 'a98-rgb', 'prophoto-rgb'):
-        max_chroma = 0.0
-        max_a = 0.0
-        max_b = 0.0
-
-        for x in range(5000):
-            # Create an achromatic RGB color
-            color = Color(space, [x / 1000] * 3)
-            if lab:
-                labish = color.convert(lab)
-                a_name, b_name = labish._space.names()[1:]
-                a = labish.get(a_name)
-                b = labish.get(b_name)
-                if abs(a) > max_a:
-                    max_a = abs(a)
-                if abs(b) > max_b:
-                    max_b = abs(b)
-
-            if lch:
-                lchish = color.convert(lch, norm=False)
-                c_name = lchish._space.names()[1]
-                chroma = lchish.get(c_name)
-                if verify:
-                    lchish_adjusted = color.convert(lch)
-                    assert lchish_adjusted.is_nan('hue'), str(lchish_adjusted) + " <-> " + str(color)
-                if chroma > max_chroma:
-                    max_chroma = chroma
-
-    if lab:
-        print('{}: maximum a: {:.53f}'.format(lab, max_a))
-        print('{}: maximum b: {:.53f}'.format(lab, max_b))
-
-    if lch:
-        num = '{:.53f}'.format(max_chroma)
-        m = RE_LEAD_ZERO.match(num)
-        minimum = '0'
-        better = '0'
-        if m and len(m.group(0)) != len(num):
-            count = len(m.group(0)[2:])
-            if int(num[m.end(0)]) == 9:
-                # Close to rolling over
-                minimum = '> 0.{}1'.format('0' * (count - 1))
-            else:
-                # Less than a decimal point of room
-                minimum = '> 0.{}{}'.format('0' * count, str(int(num[m.end(0)]) + 1))
-            # Give at least a little over a decimal point
-            better = '> 0.{}2'.format('0' * (count - 1))
-            print('{}: minimum threshold: {}'.format(lch, minimum))
-            print('{}: relaxed threshold: {}'.format(lch, better))
-        else:
-            print('{}: minimum threshold: {}'.format(lch, '?'))
-            print('{}: relaxed threshold: {}'.format(lch, '?'))
-
-        print('{}: maximum chroma: {:.53f}'.format(lch, max_chroma))
-
-        print(
-            '\n* Only potential recommendations, adjustments can be made if desired.'
-            '\n  If consraints are very tight, the minimum is probably best while the'
-            '\n  relaxed would give a little more room assuming achromatic colors even'
-            '\n  further out did not roll the next decimal point by one.'
-        )
-
-    return 0
-
-
-def main():
-    """Main."""
-
-    parser = argparse.ArgumentParser(
-        prog='calc_lch_threshold.py', description='Calculate achromatic threshold for LCh-ish colors.'
-    )
-    # Flag arguments
-    parser.add_argument(
-        '--lch', '-c', action='store', default='',
-        help="The LCh color whose 'chroma' values you'd like to evaluate."
-    )
-    parser.add_argument(
-        '--lab', '-l', action='store', default='',
-        help="Optionally view Lab color whose 'ab' values you'd like evaluate."
-    )
-    parser.add_argument(
-        '--verify', '-v', action='store_true',
-        help='Verify the space has all values equated to an achromatic hue (NaN).'
-    )
-    args = parser.parse_args()
-
-    if not args.lch and not args.lab:
-        print('No spaces provided to test!')
-
-    return run(args.lch, args.lab, args.verify)
-
-
-if __name__ == "__main__":
-    sys.exit(main())


Test patch:
diff --git a/tests/test_cam16_jmh.py b/tests/test_cam16_jmh.py
index b34dcc27..c4915a27 100644
--- a/tests/test_cam16_jmh.py
+++ b/tests/test_cam16_jmh.py
@@ -22,7 +22,7 @@ class TestCAM16JMh(util.ColorAssertsPyTest):
         ('violet', 'color(--cam16-jmh 63.507 46.779 331.39)'),
         ('white', 'color(--cam16-jmh 100 2.2369 209.53)'),
         ('gray', 'color(--cam16-jmh 43.042 1.467 209.54)'),
-        ('black', 'color(--cam16-jmh 0 0 209.54)'),
+        ('black', 'color(--cam16-jmh 0 0 0)'),
         # Test color
         ('color(--cam16-jmh 50 30 270)', 'color(--cam16-jmh 50 30 270)'),
         ('color(--cam16-jmh 50 30 270 / 0.5)', 'color(--cam16-jmh 50 30 270 / 0.5)'),
@@ -33,7 +33,7 @@ class TestCAM16JMh(util.ColorAssertsPyTest):
         ('color(--cam16-jmh 100% 100% 100)', 'color(--cam16-jmh 100 105 100 / 1)'),
         ('color(--cam16-jmh -100% -100% 100)', 'color(--cam16-jmh -100 -105 100 / 1)'),
         # Test miscellaneous cases
-        ('color(--cam16-jmh -10 none 270)', 'color(--cam16-jmh -10 0.52358 270)')
+        ('color(--cam16-jmh -10 none 270)', 'color(--cam16-jmh -10 0 270)')
     ]
 
     @pytest.mark.parametrize('color1,color2', COLORS)
@@ -100,25 +100,16 @@ def test_none_input(self):
         self.assertTrue(c.is_nan('hue'))
 
     def test_null_normalization_min_chroma(self):
-        """Test minimum saturation."""
+        """Test minimum chroma."""
 
-        c = Color(Color('white').convert('cam16-jmh').to_string()).normalize()
+        c = Color(Color('cam16-jmh', [20, 0, 270]).convert('cam16-jmh').to_string()).normalize()
         self.assertTrue(c.is_nan('hue'))
 
-        c = Color(Color('gray').convert('cam16-jmh').to_string()).normalize()
-        self.assertTrue(c.is_nan('hue'))
-
-        c = Color(Color('darkgray').convert('cam16-jmh').to_string()).normalize()
-        self.assertTrue(c.is_nan('hue'))
-
-    def test_achromatic_hue(self):
-        """Test that all RGB-ish colors convert to CAM16 JMh with a null hue."""
+    def test_null_normalization_negative_chroma(self):
+        """Test negative chroma."""
 
-        for space in ('srgb', 'display-p3', 'rec2020', 'a98-rgb', 'prophoto-rgb'):
-            for x in range(0, 256):
-                color = Color('color({space} {num:f} {num:f} {num:f})'.format(space=space, num=x / 255))
-                color2 = color.convert('cam16-jmh')
-                self.assertTrue(color2.is_nan('hue'))
+        c = Color(Color('cam16-jmh', [20, -5, 270]).convert('cam16-jmh').to_string()).normalize()
+        self.assertTrue(c.get('hue'), 270 + 180)
 
 
 class TestsAchromatic(util.ColorAsserts, unittest.TestCase):
@@ -127,7 +118,6 @@ class TestsAchromatic(util.ColorAsserts, unittest.TestCase):
     def test_achromatic(self):
         """Test when color is achromatic."""
 
-        self.assertEqual(Color('#222222').convert('cam16-jmh').is_achromatic(), True)
         self.assertEqual(
             Color('srgb', [0.000000001] * 3).convert('cam16-jmh').set('m', lambda x: x + 1e-8).is_achromatic(),
             True
@@ -136,9 +126,8 @@ def test_achromatic(self):
         self.assertEqual(Color('cam16-jmh', [0, NaN, 270]).is_achromatic(), True)
         self.assertEqual(Color('cam16-jmh', [0, 50, 270]).is_achromatic(), True)
         self.assertEqual(Color('cam16-jmh', [NaN, 50, 270]).is_achromatic(), True)
-        self.assertEqual(Color('cam16-jmh', [20, NaN, 270]).is_achromatic(), False)
+        self.assertEqual(Color('cam16-jmh', [20, NaN, 270]).is_achromatic(), True)
         self.assertEqual(Color('cam16-jmh', [NaN, NaN, 270]).is_achromatic(), True)
-        self.assertEqual(Color('cam16-jmh', [-42.841, 1.0322, 29.535]).is_achromatic(), True),
         self.assertEqual(Color('cam16-jmh', [20, -1.3, 90]).is_achromatic(), False)
 
 
diff --git a/tests/test_cam16_ucs.py b/tests/test_cam16_ucs.py
index 22c540da..72d86a27 100644
--- a/tests/test_cam16_ucs.py
+++ b/tests/test_cam16_ucs.py
@@ -299,12 +299,11 @@ class TestsAchromatic(util.ColorAsserts, unittest.TestCase):
     def test_achromatic(self):
         """Test when color is achromatic."""
 
-        self.assertEqual(Color('#222222').convert('cam16-ucs').is_achromatic(), True)
         self.assertEqual(Color('srgb', [0.000000001] * 3).convert('cam16-ucs').set('j', NaN).is_achromatic(), True)
         self.assertEqual(Color('cam16-ucs', [0, NaN, NaN]).is_achromatic(), True)
         self.assertEqual(Color('cam16-ucs', [0, NaN, NaN]).is_achromatic(), True)
         self.assertEqual(Color('cam16-ucs', [0, 3, -4]).is_achromatic(), True)
         self.assertEqual(Color('cam16-ucs', [NaN, 0, -3]).is_achromatic(), True)
-        self.assertEqual(Color('cam16-ucs', [30, NaN, 0]).is_achromatic(), False)
+        self.assertEqual(Color('cam16-ucs', [30, NaN, 0]).is_achromatic(), True)
         self.assertEqual(Color('cam16-ucs', [NaN, NaN, 0]).is_achromatic(), True)
-        self.assertEqual(Color('cam16-ucs', [-12.625, 0.40666, 0.23042]).is_achromatic(), True)
+        self.assertEqual(Color('cam16-ucs', [-12.625, 0.40666, 0.23042]).is_achromatic(), False)
diff --git a/tests/test_hct.py b/tests/test_hct.py
index ac6e70c0..b8cf6f9d 100644
--- a/tests/test_hct.py
+++ b/tests/test_hct.py
@@ -18,7 +18,7 @@ class TestHCT(util.ColorAssertsPyTest):
         ('violet', 'color(--hct 331.49 65.001 69.695)'),
         ('white', 'color(--hct 209.54 2.8716 100)'),
         ('gray', 'color(--hct 209.54 1.8977 53.585)'),
-        ('black', 'color(--hct 209.55 0 0)'),
+        ('black', 'color(--hct 0 0 0)'),
         # Very wide gamut
         ('color(--acescg 1 0 1)', 'color(--hct 342.46 146.18 63.808 / 1)'),
         ('color(--acescg 1 0 0)', 'color(--hct 25.381 186.54 58.758 / 1)'),
@@ -138,24 +138,9 @@ def test_none_input(self):
     def test_null_normalization_min_chroma(self):
         """Test minimum saturation."""
 
-        c = Color(Color('white').convert('hct').to_string()).normalize()
+        c = Color(Color('hct', [270, 0, 20]).convert('hct').to_string()).normalize()
         self.assertTrue(c.is_nan('hue'))
 
-        c = Color(Color('gray').convert('hct').to_string()).normalize()
-        self.assertTrue(c.is_nan('hue'))
-
-        c = Color(Color('darkgray').convert('hct').to_string()).normalize()
-        self.assertTrue(c.is_nan('hue'))
-
-    def test_achromatic_hue(self):
-        """Test that all RGB-ish colors convert to HCT with a null hue."""
-
-        for space in ('srgb', 'display-p3', 'rec2020', 'a98-rgb', 'prophoto-rgb'):
-            for x in range(0, 256):
-                color = Color('color({space} {num:f} {num:f} {num:f})'.format(space=space, num=x / 255))
-                color2 = color.convert('hct')
-                self.assertTrue(color2.is_nan('hue'))
-
 
 class TestsAchromatic(util.ColorAsserts, unittest.TestCase):
     """Test achromatic."""
@@ -163,7 +148,6 @@ class TestsAchromatic(util.ColorAsserts, unittest.TestCase):
     def test_achromatic(self):
         """Test when color is achromatic."""
 
-        self.assertEqual(Color('#222222').convert('hct').is_achromatic(), True)
         self.assertEqual(
             Color('srgb', [0.000000001] * 3).convert('hct').set('c', lambda x: x + 1e-8).is_achromatic(),
             True
@@ -172,7 +156,7 @@ def test_achromatic(self):
         self.assertEqual(Color('hct', [270, NaN, 0]).is_achromatic(), True)
         self.assertEqual(Color('hct', [270, 50, 0]).is_achromatic(), True)
         self.assertEqual(Color('hct', [270, 50, NaN]).is_achromatic(), True)
-        self.assertEqual(Color('hct', [270, NaN, 20]).is_achromatic(), False)
+        self.assertEqual(Color('hct', [270, NaN, 20]).is_achromatic(), True)
         self.assertEqual(Color('hct', [270, NaN, NaN]).is_achromatic(), True)
-        self.assertEqual(Color('hct', [29.546, 0.60569, -9.0536]).is_achromatic(), True),
+        self.assertEqual(Color('hct', [29.546, 0.60569, -9.0536]).is_achromatic(), False),
         self.assertEqual(Color('hct', [270, -20, 50]).is_achromatic(), False)
diff --git a/tests/test_ipt.py b/tests/test_ipt.py
index d91b1e5e..52be6e0c 100644
--- a/tests/test_ipt.py
+++ b/tests/test_ipt.py
@@ -103,11 +103,10 @@ class TestsAchromatic(util.ColorAsserts, unittest.TestCase):
     def test_achromatic(self):
         """Test when color is achromatic."""
 
-        self.assertEqual(Color('#464646').convert('ipt').is_achromatic(), True)
         self.assertEqual(Color('ipt', [0, NaN, NaN]).is_achromatic(), True)
         self.assertEqual(Color('ipt', [0, NaN, NaN]).is_achromatic(), True)
         self.assertEqual(Color('ipt', [0, 0.3, -0.4]).is_achromatic(), False)
         self.assertEqual(Color('ipt', [NaN, 0, -0.3]).is_achromatic(), False)
-        self.assertEqual(Color('ipt', [0.3, NaN, 0]).is_achromatic(), False)
+        self.assertEqual(Color('ipt', [0.3, NaN, 0]).is_achromatic(), True)
         self.assertEqual(Color('ipt', [NaN, NaN, 0]).is_achromatic(), True)
         self.assertEqual(Color('ipt', [-0.5, NaN, NaN]).is_achromatic(), True)
diff --git a/tests/test_jzazbz.py b/tests/test_jzazbz.py
index 208f50d3..b81a75ad 100644
--- a/tests/test_jzazbz.py
+++ b/tests/test_jzazbz.py
@@ -103,13 +103,11 @@ class TestsAchromatic(util.ColorAsserts, unittest.TestCase):
     def test_achromatic(self):
         """Test when color is achromatic."""
 
-        self.assertEqual(Color('#222222').convert('jzazbz').is_achromatic(), True)
-        self.assertEqual(Color('#222222').convert('jzazbz').set('jz', NaN).is_achromatic(), False)
         self.assertEqual(Color('jzazbz', [0, NaN, NaN]).is_achromatic(), True)
         self.assertEqual(Color('jzazbz', [0, NaN, NaN]).is_achromatic(), True)
         self.assertEqual(Color('jzazbz', [0, 0.3, -0.4]).is_achromatic(), False)
         self.assertEqual(Color('jzazbz', [NaN, 0, -0.3]).is_achromatic(), False)
         self.assertEqual(Color('jzazbz', [NaN, 0, 0]).is_achromatic(), True)
-        self.assertEqual(Color('jzazbz', [0.3, NaN, 0]).is_achromatic(), False)
+        self.assertEqual(Color('jzazbz', [0.3, NaN, 0]).is_achromatic(), True)
         self.assertEqual(Color('jzazbz', [NaN, NaN, 0]).is_achromatic(), True)
         self.assertEqual(Color('jzazbz', [-0.05, 0, 0]).is_achromatic(), True)
diff --git a/tests/test_jzczhz.py b/tests/test_jzczhz.py
index dbe0b82e..aafcdbf9 100644
--- a/tests/test_jzczhz.py
+++ b/tests/test_jzczhz.py
@@ -17,8 +17,8 @@ class TestJzCzhz(util.ColorAssertsPyTest):
         ('blue', 'color(jzczhz 0.09577 0.19029 257.61)'),
         ('indigo', 'color(jzczhz 0.06146 0.10408 287.05)'),
         ('violet', 'color(jzczhz 0.16771 0.08468 319.37)'),
-        ('white', 'color(jzczhz 0.22207 0.0002 none)'),
-        ('gray', 'color(jzczhz 0.11827 0.00014 none)'),
+        ('white', 'color(jzczhz 0.22207 0.0002 216.08)'),
+        ('gray', 'color(jzczhz 0.11827 0.00014 216.08)'),
         ('black', 'color(jzczhz 0 0 none)'),
         # Test color
         ('color(jzczhz 1 0.3 270)', 'color(jzczhz 1 0.3 270)'),
@@ -122,24 +122,9 @@ def test_none_input(self):
     def test_null_normalization_min_chroma(self):
         """Test minimum saturation."""
 
-        c = Color(Color('white').convert('jzczhz').to_string(precision=6)).normalize()
+        c = Color(Color('jzczhz', [0.2, 0, 270]).convert('jzczhz').to_string(precision=6)).normalize()
         self.assertTrue(c.is_nan('hue'))
 
-        c = Color(Color('gray').convert('jzczhz').to_string(precision=6)).normalize()
-        self.assertTrue(c.is_nan('hue'))
-
-        c = Color(Color('darkgray').convert('jzczhz').to_string(precision=6)).normalize()
-        self.assertTrue(c.is_nan('hue'))
-
-    def test_achromatic_hue(self):
-        """Test that all RGB-ish colors convert to OkLCh with a null hue."""
-
-        for space in ('srgb', 'display-p3', 'rec2020', 'a98-rgb', 'prophoto-rgb'):
-            for x in range(0, 256):
-                color = Color('color({space} {num:f} {num:f} {num:f})'.format(space=space, num=x / 255))
-                color2 = color.convert('jzczhz')
-                self.assertTrue(color2.is_nan('hue'))
-
 
 class TestQuirks(util.ColorAsserts, unittest.TestCase):
     """Test quirks."""
@@ -175,12 +160,11 @@ def test_achromatic(self):
             Color('#222222').convert('jzczhz').set('cz', lambda c: -c).set('h', lambda h: h + 180).is_achromatic(),
             True
         )
-        self.assertEqual(Color('srgb', [5.2, 5.2, 5.2]).convert('jzczhz').is_achromatic(), True)
         self.assertEqual(Color('jzczhz', [NaN, 0.0, 270]).is_achromatic(), True)
         self.assertEqual(Color('jzczhz', [0, NaN, 270]).is_achromatic(), True)
         self.assertEqual(Color('jzczhz', [0, 0.5, 270]).is_achromatic(), False)
         self.assertEqual(Color('pink').convert('jzczhz').is_achromatic(), False)
         self.assertEqual(Color('jzczhz', [NaN, 0.5, 270]).is_achromatic(), False)
-        self.assertEqual(Color('jzczhz', [0.2, NaN, 270]).is_achromatic(), False)
+        self.assertEqual(Color('jzczhz', [0.2, NaN, 270]).is_achromatic(), True)
         self.assertEqual(Color('jzczhz', [NaN, NaN, 270]).is_achromatic(), True)
         self.assertEqual(Color('jzczhz', [-0.05, 0, 0]).is_achromatic(), True)
diff --git a/tests/test_lch.py b/tests/test_lch.py
index e79d194d..9b666fd5 100644
--- a/tests/test_lch.py
+++ b/tests/test_lch.py
@@ -158,6 +158,12 @@ def test_null_normalization_min_chroma(self):
         c = Color('lch(90% 0 120 / 1)').normalize()
         self.assertTrue(c.is_nan('hue'))
 
+    def test_null_normalization_negative_chroma(self):
+        """Test minimum saturation."""
+
+        c = Color('lch(90% -10 120 / 1)').normalize()
+        self.assertColorEqual(c, Color('lch(90% 10 300)'))
+
     def test_from_lab(self):
         """Test null from Lab conversion."""