srgb 0.3.3

sRGB primitives and constants — lightweight crate with functions and constants needed when manipulating sRGB colours
Documentation
import struct
import subprocess
import threading

import numpy
import scipy.optimize


_INF = float('inf')


def bits_from_float(value: float) -> int:
        return struct.unpack('>l', struct.pack('>f', value))[0]


class Scorer:
        def __init__(self) -> None:
                self._process = subprocess.Popen(('./gamma8',),
                                                 stdin=subprocess.PIPE,
                                                 stdout=subprocess.PIPE)
                self._lock = threading.Lock()

        def __call__(self, *args) -> float:
                if len(args) == 5:
                        min_approx, min_255, bits_offset, bits_step, y_offset = args
                elif len(args) == 4:
                        min_approx, min_255, bits_offset, y_offset = args
                        bits_step = 1 << 18
                else:
                        raise TypeError()

                if min_approx <= 0.001 or min_approx >= 1.0:
                        return _INF
                if min_255 <= 0.001 or min_255 >= 1.0:
                        return _INF
                if bits_offset < 0.001 or bits_offset > 0.1:
                        return _INF
                if bits_offset > min_255:
                        return _INF
                if bits_step <= 0 or bits_step >= 2**26:
                        return _INF
                if y_offset < -1.0 or y_offset > 1.0:
                        return _INF

                values = (float(min_approx), float(min_255),
                          float(bits_offset), int(bits_step),
                          float(y_offset))
                line = (' '.join(str(value) for value in values) + '\n').encode('ascii')

                with self._lock:
                        proc = self._process
                        assert proc, 'Scorer already closed'
                        proc.stdin.write(line)
                        proc.stdin.flush()
                        line = proc.stdout.readline()

                return float(line.split()[0]) * 2**14

        def close(self) -> None:
                with self._lock:
                        proc = self._process
                        self._process = None
                if proc:
                        proc.stdout.close()
                        try:
                                proc.wait(1)
                        except subprocess.TimeoutExpired:
                                proc.kill()
                                proc.wait(1)

        def __enter__(self) -> 'Scorer':
                return self

        def __exit__(self, *_):
                self.close()


def main():
        with Scorer() as scorer:
                # print(scorer(0.003187033, 0.99554527, 0.003166763111948967, 524288, 0.514))
                # print(scorer(0.00318697230, 0.995502096, 0.00309117556, 522095, 0.509272519))

                # print(scorer(0.00318697230, 0.995502096, 0.00309117556, 0.509272519))
                # print(scorer(0.00318701, 0.99549563, 0.00311433, 0.50983687))

                x0 = numpy.array([0.00319052, 0.99549606, 0.00313369, 0.50994874])
                best = scorer(*x0)
                print(best)

                res = scipy.optimize.minimize((lambda x: scorer(*x)), x0, method='nelder-mead',
                                              options={'disp': True})
                print(repr(res))
                print('{}{}'.format(res.fun, ' improved' if res.fun < best else ''))
                print('[{}]'.format(', '.join(str(value) for value in res.x)))
                print('[{}]'.format(', '.join('0x{:x}'.format(bits_from_float(value)) for value in res.x)))


if __name__ == '__main__':
        main()