File: fh.py
   1 #!/usr/bin/python
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the "Software"), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 # mpl-colormaps by Nathaniel Smith & Stefan van der Walt
  26 #
  27 # To the extent possible under law, the persons who associated CC0 with
  28 # mpl-colormaps have waived all copyright and related or neighboring rights
  29 # to mpl-colormaps.
  30 #
  31 # You should have received a copy of the CC0 legalcode along with this
  32 # work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
  33 
  34 # Creative Commons Legal Code
  35 #
  36 # CC0 1.0 Universal
  37 #
  38 #     CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
  39 #     LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
  40 #     ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
  41 #     INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
  42 #     REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
  43 #     PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
  44 #     THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
  45 #     HEREUNDER.
  46 #
  47 # Statement of Purpose
  48 #
  49 # The laws of most jurisdictions throughout the world automatically confer
  50 # exclusive Copyright and Related Rights (defined below) upon the creator
  51 # and subsequent owner(s) (each and all, an "owner") of an original work of
  52 # authorship and/or a database (each, a "Work").
  53 #
  54 # Certain owners wish to permanently relinquish those rights to a Work for
  55 # the purpose of contributing to a commons of creative, cultural and
  56 # scientific works ("Commons") that the public can reliably and without fear
  57 # of later claims of infringement build upon, modify, incorporate in other
  58 # works, reuse and redistribute as freely as possible in any form whatsoever
  59 # and for any purposes, including without limitation commercial purposes.
  60 # These owners may contribute to the Commons to promote the ideal of a free
  61 # culture and the further production of creative, cultural and scientific
  62 # works, or to gain reputation or greater distribution for their Work in
  63 # part through the use and efforts of others.
  64 #
  65 # For these and/or other purposes and motivations, and without any
  66 # expectation of additional consideration or compensation, the person
  67 # associating CC0 with a Work (the "Affirmer"), to the extent that he or she
  68 # is an owner of Copyright and Related Rights in the Work, voluntarily
  69 # elects to apply CC0 to the Work and publicly distribute the Work under its
  70 # terms, with knowledge of his or her Copyright and Related Rights in the
  71 # Work and the meaning and intended legal effect of CC0 on those rights.
  72 #
  73 # 1. Copyright and Related Rights. A Work made available under CC0 may be
  74 # protected by copyright and related or neighboring rights ("Copyright and
  75 # Related Rights"). Copyright and Related Rights include, but are not
  76 # limited to, the following:
  77 #
  78 #   i. the right to reproduce, adapt, distribute, perform, display,
  79 #      communicate, and translate a Work;
  80 #  ii. moral rights retained by the original author(s) and/or performer(s);
  81 # iii. publicity and privacy rights pertaining to a person's image or
  82 #      likeness depicted in a Work;
  83 #  iv. rights protecting against unfair competition in regards to a Work,
  84 #      subject to the limitations in paragraph 4(a), below;
  85 #   v. rights protecting the extraction, dissemination, use and reuse of data
  86 #      in a Work;
  87 #  vi. database rights (such as those arising under Directive 96/9/EC of the
  88 #      European Parliament and of the Council of 11 March 1996 on the legal
  89 #      protection of databases, and under any national implementation
  90 #      thereof, including any amended or successor version of such
  91 #      directive); and
  92 # vii. other similar, equivalent or corresponding rights throughout the
  93 #      world based on applicable law or treaty, and any national
  94 #      implementations thereof.
  95 #
  96 # 2. Waiver. To the greatest extent permitted by, but not in contravention
  97 # of, applicable law, Affirmer hereby overtly, fully, permanently,
  98 # irrevocably and unconditionally waives, abandons, and surrenders all of
  99 # Affirmer's Copyright and Related Rights and associated claims and causes
 100 # of action, whether now known or unknown (including existing as well as
 101 # future claims and causes of action), in the Work (i) in all territories
 102 # worldwide, (ii) for the maximum duration provided by applicable law or
 103 # treaty (including future time extensions), (iii) in any current or future
 104 # medium and for any number of copies, and (iv) for any purpose whatsoever,
 105 # including without limitation commercial, advertising or promotional
 106 # purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
 107 # member of the public at large and to the detriment of Affirmer's heirs and
 108 # successors, fully intending that such Waiver shall not be subject to
 109 # revocation, rescission, cancellation, termination, or any other legal or
 110 # equitable action to disrupt the quiet enjoyment of the Work by the public
 111 # as contemplated by Affirmer's express Statement of Purpose.
 112 #
 113 # 3. Public License Fallback. Should any part of the Waiver for any reason
 114 # be judged legally invalid or ineffective under applicable law, then the
 115 # Waiver shall be preserved to the maximum extent permitted taking into
 116 # account Affirmer's express Statement of Purpose. In addition, to the
 117 # extent the Waiver is so judged Affirmer hereby grants to each affected
 118 # person a royalty-free, non transferable, non sublicensable, non exclusive,
 119 # irrevocable and unconditional license to exercise Affirmer's Copyright and
 120 # Related Rights in the Work (i) in all territories worldwide, (ii) for the
 121 # maximum duration provided by applicable law or treaty (including future
 122 # time extensions), (iii) in any current or future medium and for any number
 123 # of copies, and (iv) for any purpose whatsoever, including without
 124 # limitation commercial, advertising or promotional purposes (the
 125 # "License"). The License shall be deemed effective as of the date CC0 was
 126 # applied by Affirmer to the Work. Should any part of the License for any
 127 # reason be judged legally invalid or ineffective under applicable law, such
 128 # partial invalidity or ineffectiveness shall not invalidate the remainder
 129 # of the License, and in such case Affirmer hereby affirms that he or she
 130 # will not (i) exercise any of his or her remaining Copyright and Related
 131 # Rights in the Work or (ii) assert any associated claims and causes of
 132 # action with respect to the Work, in either case contrary to Affirmer's
 133 # express Statement of Purpose.
 134 #
 135 # 4. Limitations and Disclaimers.
 136 #
 137 #  a. No trademark or patent rights held by Affirmer are waived, abandoned,
 138 #     surrendered, licensed or otherwise affected by this document.
 139 #  b. Affirmer offers the Work as-is and makes no representations or
 140 #     warranties of any kind concerning the Work, express, implied,
 141 #     statutory or otherwise, including without limitation warranties of
 142 #     title, merchantability, fitness for a particular purpose, non
 143 #     infringement, or the absence of latent or other defects, accuracy, or
 144 #     the present or absence of errors, whether or not discoverable, all to
 145 #     the greatest extent permissible under applicable law.
 146 #  c. Affirmer disclaims responsibility for clearing rights of other persons
 147 #     that may apply to the Work or any use thereof, including without
 148 #     limitation any person's Copyright and Related Rights in the Work.
 149 #     Further, Affirmer disclaims responsibility for obtaining any necessary
 150 #     consents, permissions or other rights required for any use of the
 151 #     Work.
 152 #  d. Affirmer understands and acknowledges that Creative Commons is not a
 153 #     party to this document and has no duty or obligation with respect to
 154 #     this CC0 or use of the Work.
 155 
 156 
 157 info = '''
 158 fh [options...] [python expression]
 159 
 160 
 161 Function Heatmapper emits a picture showing a grayscale-heatmap view of the
 162 function f(x, y) implied by the Python math-expression given. Several math
 163 functions and constants are available, all their names being lowercase.
 164 
 165 The output is always an uncompressed BMP (bitmap) in RGB (24-bit) mode, and
 166 with a resolution of 3840x2160, or 2160x2160 if using option `-square`, or
 167 one of its aliases.
 168 
 169 The formula/expression can use floating-point values `x` and `y`, to get
 170 the cartesian position each time. All names (functions and values) from
 171 the built-in python modules `math` and `random` are also available to the
 172 expression run.
 173 
 174 By default `x` and `y` are both sampled regularly from the set 0..1, but
 175 those can be changed using the relevant options.
 176 
 177 The formula is expected to result in floating-point values in a finite
 178 range, as the final colors will be `scaled` according to the span/range of
 179 values the formula gave in the sampled sub-domain of the (x, y) values.
 180 
 181 While many of the examples below save to files, you can just pipe to apps
 182 which support the BMP format, which should be the vast majority of picture
 183 viewers and media players, such as mpv.
 184 
 185 
 186 Options
 187 
 188 All options can start with either a single or a double leading dash:
 189 
 190     -h, -help         show this help message
 191     -hd, -1080p       use a 1920x1080 resolution, or 1080x1080
 192     -s, -square       change output width to be the same as the height
 193     -x=[min..max]     change span of the sampled x values
 194     -xy=[min..max]    change spans of the sampled x and y values
 195     -y=[min..max]     change span of the sampled y values
 196 
 197 
 198 Examples
 199 
 200 
 201 fh 'x/(x+y)' > corner-fan-1.png
 202 
 203 fh 'y/(x+y)' > corner-fan-2.png
 204 
 205 fh 'abs(sin(wrap(x, -5, 5)) / wrap(y, 1, 5)**1.4' > floor-lights.png
 206 
 207 fh 'lbeta(wrap(x, -5, 5) + 5.1, wrap(y, 1, 5) + 5.1)' > beta-gradient.png
 208 
 209 fh 'lcm(ceil(wrap(x, 0.01, 199.99)), ceil(wrap(y, 0.01, 199.99)))' > lcm.png
 210 
 211 fh 'abs(wrap(x, -5, 5)) + sqrt(abs(sin(2*wrap(y, -5, 5))))' > bars.png
 212 
 213 fh 'sign(cos(160*tau*x) + sin(90*tau*y))' > 90-deg-square-tiles.png
 214 
 215 fh 'gauss(sin(50*x) * cos(50*9/16*y) + 1)' > light-tiles.png
 216 
 217 # central ripple
 218 fh --square 'sinc(0.55 * hypot(wrap(x, -10, 10), wrap(x, -10, 10)))' > cr.png
 219 '''
 220 
 221 
 222 from sys import argv, stdout
 223 
 224 
 225 # handle standard help cmd-line options, quitting right away in that case
 226 if len(argv) > 1 and argv[1] in ('-h', '--h', '-help', '--help'):
 227     print(info.strip())
 228     exit(0)
 229 
 230 
 231 import math
 232 from math import \
 233     acos, acosh, asin, asinh, atan, atan2, atanh, ceil, comb, \
 234     copysign, cos, cosh, degrees, dist, e, erf, erfc, exp, expm1, \
 235     fabs, factorial, floor, fmod, frexp, fsum, gamma, gcd, hypot, inf, \
 236     isclose, isfinite, isinf, isnan, isqrt, lcm, ldexp, lgamma, log, \
 237     log10, log1p, log2, modf, nan, nextafter, perm, pi, pow, prod, \
 238     radians, remainder, sin, sinh, sqrt, tan, tanh, tau, trunc, ulp
 239 try:
 240     from math import cbrt, exp2
 241 except Exception as _:
 242     pass
 243 
 244 math_log = log
 245 math_sqrt = sqrt
 246 
 247 from random import \
 248     betavariate, choice, choices, expovariate, gammavariate, gauss, \
 249     getrandbits, getstate, lognormvariate, normalvariate, paretovariate, \
 250     randbytes, randint, random, randrange, sample, seed, setstate, \
 251     shuffle, triangular, uniform, vonmisesvariate, weibullvariate
 252 
 253 from struct import pack
 254 
 255 
 256 # I'm using data straight from the original implementation of Viridis
 257 # by Nathaniel Smith & Stefan van der Walt:
 258 # https://github.com/BIDS/colormap/blob/master/option_d.py
 259 viridis = [
 260     0.26700401, 0.00487433, 0.32941519, 0.26851048, 0.00960483, 0.33542652,
 261     0.26994384, 0.01462494, 0.34137895, 0.27130489, 0.01994186, 0.34726862,
 262     0.27259384, 0.02556309, 0.35309303, 0.27380934, 0.03149748, 0.35885256,
 263     0.27495242, 0.03775181, 0.36454323, 0.27602238, 0.04416723, 0.37016418,
 264     0.2770184, 0.05034437, 0.37571452, 0.27794143, 0.05632444, 0.38119074,
 265     0.27879067, 0.06214536, 0.38659204, 0.2795655, 0.06783587, 0.39191723,
 266     0.28026658, 0.07341724, 0.39716349, 0.28089358, 0.07890703, 0.40232944,
 267     0.28144581, 0.0843197, 0.40741404, 0.28192358, 0.08966622, 0.41241521,
 268     0.28232739, 0.09495545, 0.41733086, 0.28265633, 0.10019576, 0.42216032,
 269     0.28291049, 0.10539345, 0.42690202, 0.28309095, 0.11055307, 0.43155375,
 270     0.28319704, 0.11567966, 0.43611482, 0.28322882, 0.12077701, 0.44058404,
 271     0.28318684, 0.12584799, 0.44496, 0.283072, 0.13089477, 0.44924127,
 272     0.28288389, 0.13592005, 0.45342734, 0.28262297, 0.14092556, 0.45751726,
 273     0.28229037, 0.14591233, 0.46150995, 0.28188676, 0.15088147, 0.46540474,
 274     0.28141228, 0.15583425, 0.46920128, 0.28086773, 0.16077132, 0.47289909,
 275     0.28025468, 0.16569272, 0.47649762, 0.27957399, 0.17059884, 0.47999675,
 276     0.27882618, 0.1754902, 0.48339654, 0.27801236, 0.18036684, 0.48669702,
 277     0.27713437, 0.18522836, 0.48989831, 0.27619376, 0.19007447, 0.49300074,
 278     0.27519116, 0.1949054, 0.49600488, 0.27412802, 0.19972086, 0.49891131,
 279     0.27300596, 0.20452049, 0.50172076, 0.27182812, 0.20930306, 0.50443413,
 280     0.27059473, 0.21406899, 0.50705243, 0.26930756, 0.21881782, 0.50957678,
 281     0.26796846, 0.22354911, 0.5120084, 0.26657984, 0.2282621, 0.5143487,
 282     0.2651445, 0.23295593, 0.5165993, 0.2636632, 0.23763078, 0.51876163,
 283     0.26213801, 0.24228619, 0.52083736, 0.26057103, 0.2469217, 0.52282822,
 284     0.25896451, 0.25153685, 0.52473609, 0.25732244, 0.2561304, 0.52656332,
 285     0.25564519, 0.26070284, 0.52831152, 0.25393498, 0.26525384, 0.52998273,
 286     0.25219404, 0.26978306, 0.53157905, 0.25042462, 0.27429024, 0.53310261,
 287     0.24862899, 0.27877509, 0.53455561, 0.2468114, 0.28323662, 0.53594093,
 288     0.24497208, 0.28767547, 0.53726018, 0.24311324, 0.29209154, 0.53851561,
 289     0.24123708, 0.29648471, 0.53970946, 0.23934575, 0.30085494, 0.54084398,
 290     0.23744138, 0.30520222, 0.5419214, 0.23552606, 0.30952657, 0.54294396,
 291     0.23360277, 0.31382773, 0.54391424, 0.2316735, 0.3181058, 0.54483444,
 292     0.22973926, 0.32236127, 0.54570633, 0.22780192, 0.32659432, 0.546532,
 293     0.2258633, 0.33080515, 0.54731353, 0.22392515, 0.334994, 0.54805291,
 294     0.22198915, 0.33916114, 0.54875211, 0.22005691, 0.34330688, 0.54941304,
 295     0.21812995, 0.34743154, 0.55003755, 0.21620971, 0.35153548, 0.55062743,
 296     0.21429757, 0.35561907, 0.5511844, 0.21239477, 0.35968273, 0.55171011,
 297     0.2105031, 0.36372671, 0.55220646, 0.20862342, 0.36775151, 0.55267486,
 298     0.20675628, 0.37175775, 0.55311653, 0.20490257, 0.37574589, 0.55353282,
 299     0.20306309, 0.37971644, 0.55392505, 0.20123854, 0.38366989, 0.55429441,
 300     0.1994295, 0.38760678, 0.55464205, 0.1976365, 0.39152762, 0.55496905,
 301     0.19585993, 0.39543297, 0.55527637, 0.19410009, 0.39932336, 0.55556494,
 302     0.19235719, 0.40319934, 0.55583559, 0.19063135, 0.40706148, 0.55608907,
 303     0.18892259, 0.41091033, 0.55632606, 0.18723083, 0.41474645, 0.55654717,
 304     0.18555593, 0.4185704, 0.55675292, 0.18389763, 0.42238275, 0.55694377,
 305     0.18225561, 0.42618405, 0.5571201, 0.18062949, 0.42997486, 0.55728221,
 306     0.17901879, 0.43375572, 0.55743035, 0.17742298, 0.4375272, 0.55756466,
 307     0.17584148, 0.44128981, 0.55768526, 0.17427363, 0.4450441, 0.55779216,
 308     0.17271876, 0.4487906, 0.55788532, 0.17117615, 0.4525298, 0.55796464,
 309     0.16964573, 0.45626209, 0.55803034, 0.16812641, 0.45998802, 0.55808199,
 310     0.1666171, 0.46370813, 0.55811913, 0.16511703, 0.4674229, 0.55814141,
 311     0.16362543, 0.47113278, 0.55814842, 0.16214155, 0.47483821, 0.55813967,
 312     0.16066467, 0.47853961, 0.55811466, 0.15919413, 0.4822374, 0.5580728,
 313     0.15772933, 0.48593197, 0.55801347, 0.15626973, 0.4896237, 0.557936,
 314     0.15481488, 0.49331293, 0.55783967, 0.15336445, 0.49700003, 0.55772371,
 315     0.1519182, 0.50068529, 0.55758733, 0.15047605, 0.50436904, 0.55742968,
 316     0.14903918, 0.50805136, 0.5572505, 0.14760731, 0.51173263, 0.55704861,
 317     0.14618026, 0.51541316, 0.55682271, 0.14475863, 0.51909319, 0.55657181,
 318     0.14334327, 0.52277292, 0.55629491, 0.14193527, 0.52645254, 0.55599097,
 319     0.14053599, 0.53013219, 0.55565893, 0.13914708, 0.53381201, 0.55529773,
 320     0.13777048, 0.53749213, 0.55490625, 0.1364085, 0.54117264, 0.55448339,
 321     0.13506561, 0.54485335, 0.55402906, 0.13374299, 0.54853458, 0.55354108,
 322     0.13244401, 0.55221637, 0.55301828, 0.13117249, 0.55589872, 0.55245948,
 323     0.1299327, 0.55958162, 0.55186354, 0.12872938, 0.56326503, 0.55122927,
 324     0.12756771, 0.56694891, 0.55055551, 0.12645338, 0.57063316, 0.5498411,
 325     0.12539383, 0.57431754, 0.54908564, 0.12439474, 0.57800205, 0.5482874,
 326     0.12346281, 0.58168661, 0.54744498, 0.12260562, 0.58537105, 0.54655722,
 327     0.12183122, 0.58905521, 0.54562298, 0.12114807, 0.59273889, 0.54464114,
 328     0.12056501, 0.59642187, 0.54361058, 0.12009154, 0.60010387, 0.54253043,
 329     0.11973756, 0.60378459, 0.54139999, 0.11951163, 0.60746388, 0.54021751,
 330     0.11942341, 0.61114146, 0.53898192, 0.11948255, 0.61481702, 0.53769219,
 331     0.11969858, 0.61849025, 0.53634733, 0.12008079, 0.62216081, 0.53494633,
 332     0.12063824, 0.62582833, 0.53348834, 0.12137972, 0.62949242, 0.53197275,
 333     0.12231244, 0.63315277, 0.53039808, 0.12344358, 0.63680899, 0.52876343,
 334     0.12477953, 0.64046069, 0.52706792, 0.12632581, 0.64410744, 0.52531069,
 335     0.12808703, 0.64774881, 0.52349092, 0.13006688, 0.65138436, 0.52160791,
 336     0.13226797, 0.65501363, 0.51966086, 0.13469183, 0.65863619, 0.5176488,
 337     0.13733921, 0.66225157, 0.51557101, 0.14020991, 0.66585927, 0.5134268,
 338     0.14330291, 0.66945881, 0.51121549, 0.1466164, 0.67304968, 0.50893644,
 339     0.15014782, 0.67663139, 0.5065889, 0.15389405, 0.68020343, 0.50417217,
 340     0.15785146, 0.68376525, 0.50168574, 0.16201598, 0.68731632, 0.49912906,
 341     0.1663832, 0.69085611, 0.49650163, 0.1709484, 0.69438405, 0.49380294,
 342     0.17570671, 0.6978996, 0.49103252, 0.18065314, 0.70140222, 0.48818938,
 343     0.18578266, 0.70489133, 0.48527326, 0.19109018, 0.70836635, 0.48228395,
 344     0.19657063, 0.71182668, 0.47922108, 0.20221902, 0.71527175, 0.47608431,
 345     0.20803045, 0.71870095, 0.4728733, 0.21400015, 0.72211371, 0.46958774,
 346     0.22012381, 0.72550945, 0.46622638, 0.2263969, 0.72888753, 0.46278934,
 347     0.23281498, 0.73224735, 0.45927675, 0.2393739, 0.73558828, 0.45568838,
 348     0.24606968, 0.73890972, 0.45202405, 0.25289851, 0.74221104, 0.44828355,
 349     0.25985676, 0.74549162, 0.44446673, 0.26694127, 0.74875084, 0.44057284,
 350     0.27414922, 0.75198807, 0.4366009, 0.28147681, 0.75520266, 0.43255207,
 351     0.28892102, 0.75839399, 0.42842626, 0.29647899, 0.76156142, 0.42422341,
 352     0.30414796, 0.76470433, 0.41994346, 0.31192534, 0.76782207, 0.41558638,
 353     0.3198086, 0.77091403, 0.41115215, 0.3277958, 0.77397953, 0.40664011,
 354     0.33588539, 0.7770179, 0.40204917, 0.34407411, 0.78002855, 0.39738103,
 355     0.35235985, 0.78301086, 0.39263579, 0.36074053, 0.78596419, 0.38781353,
 356     0.3692142, 0.78888793, 0.38291438, 0.37777892, 0.79178146, 0.3779385,
 357     0.38643282, 0.79464415, 0.37288606, 0.39517408, 0.79747541, 0.36775726,
 358     0.40400101, 0.80027461, 0.36255223, 0.4129135, 0.80304099, 0.35726893,
 359     0.42190813, 0.80577412, 0.35191009, 0.43098317, 0.80847343, 0.34647607,
 360     0.44013691, 0.81113836, 0.3409673, 0.44936763, 0.81376835, 0.33538426,
 361     0.45867362, 0.81636288, 0.32972749, 0.46805314, 0.81892143, 0.32399761,
 362     0.47750446, 0.82144351, 0.31819529, 0.4870258, 0.82392862, 0.31232133,
 363     0.49661536, 0.82637633, 0.30637661, 0.5062713, 0.82878621, 0.30036211,
 364     0.51599182, 0.83115784, 0.29427888, 0.52577622, 0.83349064, 0.2881265,
 365     0.5356211, 0.83578452, 0.28190832, 0.5455244, 0.83803918, 0.27562602,
 366     0.55548397, 0.84025437, 0.26928147, 0.5654976, 0.8424299, 0.26287683,
 367     0.57556297, 0.84456561, 0.25641457, 0.58567772, 0.84666139, 0.24989748,
 368     0.59583934, 0.84871722, 0.24332878, 0.60604528, 0.8507331, 0.23671214,
 369     0.61629283, 0.85270912, 0.23005179, 0.62657923, 0.85464543, 0.22335258,
 370     0.63690157, 0.85654226, 0.21662012, 0.64725685, 0.85839991, 0.20986086,
 371     0.65764197, 0.86021878, 0.20308229, 0.66805369, 0.86199932, 0.19629307,
 372     0.67848868, 0.86374211, 0.18950326, 0.68894351, 0.86544779, 0.18272455,
 373     0.69941463, 0.86711711, 0.17597055, 0.70989842, 0.86875092, 0.16925712,
 374     0.72039115, 0.87035015, 0.16260273, 0.73088902, 0.87191584, 0.15602894,
 375     0.74138803, 0.87344918, 0.14956101, 0.75188414, 0.87495143, 0.14322828,
 376     0.76237342, 0.87642392, 0.13706449, 0.77285183, 0.87786808, 0.13110864,
 377     0.78331535, 0.87928545, 0.12540538, 0.79375994, 0.88067763, 0.12000532,
 378     0.80418159, 0.88204632, 0.11496505, 0.81457634, 0.88339329, 0.11034678,
 379     0.82494028, 0.88472036, 0.10621724, 0.83526959, 0.88602943, 0.1026459,
 380     0.84556056, 0.88732243, 0.09970219, 0.8558096, 0.88860134, 0.09745186,
 381     0.86601325, 0.88986815, 0.09595277, 0.87616824, 0.89112487, 0.09525046,
 382     0.88627146, 0.89237353, 0.09537439, 0.89632002, 0.89361614, 0.09633538,
 383     0.90631121, 0.89485467, 0.09812496, 0.91624212, 0.89609127, 0.1007168,
 384     0.92610579, 0.89732977, 0.10407067, 0.93590444, 0.8985704, 0.10813094,
 385     0.94563626, 0.899815, 0.11283773, 0.95529972, 0.90106534, 0.11812832,
 386     0.96489353, 0.90232311, 0.12394051, 0.97441665, 0.90358991, 0.13021494,
 387     0.98386829, 0.90486726, 0.13689671, 0.99324789, 0.90615657, 0.1439362,
 388 ]
 389 
 390 # I'm using data straight from the original implementation of Parula
 391 # by Nathaniel Smith & Stefan van der Walt:
 392 # https://github.com/BIDS/colormap/blob/master/parula.py
 393 parula = [
 394     0.2081, 0.1663, 0.5292, 0.2116238095, 0.1897809524, 0.5776761905,
 395     0.212252381, 0.2137714286, 0.6269714286, 0.2081, 0.2386, 0.6770857143,
 396     0.1959047619, 0.2644571429, 0.7279, 0.1707285714, 0.2919380952,
 397     0.779247619, 0.1252714286, 0.3242428571, 0.8302714286,
 398     0.0591333333, 0.3598333333, 0.8683333333, 0.0116952381, 0.3875095238,
 399     0.8819571429, 0.0059571429, 0.4086142857, 0.8828428571,
 400     0.0165142857, 0.4266, 0.8786333333, 0.032852381, 0.4430428571,
 401     0.8719571429, 0.0498142857, 0.4585714286, 0.8640571429,
 402     0.0629333333, 0.4736904762, 0.8554380952, 0.0722666667, 0.4886666667,
 403     0.8467, 0.0779428571, 0.5039857143, 0.8383714286,
 404     0.079347619, 0.5200238095, 0.8311809524, 0.0749428571, 0.5375428571,
 405     0.8262714286, 0.0640571429, 0.5569857143, 0.8239571429,
 406     0.0487714286, 0.5772238095, 0.8228285714, 0.0343428571, 0.5965809524,
 407     0.819852381, 0.0265, 0.6137, 0.8135, 0.0238904762, 0.6286619048,
 408     0.8037619048, 0.0230904762, 0.6417857143, 0.7912666667,
 409     0.0227714286, 0.6534857143, 0.7767571429, 0.0266619048, 0.6641952381,
 410     0.7607190476, 0.0383714286, 0.6742714286, 0.743552381,
 411     0.0589714286, 0.6837571429, 0.7253857143,
 412     0.0843, 0.6928333333, 0.7061666667, 0.1132952381, 0.7015, 0.6858571429,
 413     0.1452714286, 0.7097571429, 0.6646285714, 0.1801333333, 0.7176571429,
 414     0.6424333333, 0.2178285714, 0.7250428571, 0.6192619048,
 415     0.2586428571, 0.7317142857, 0.5954285714, 0.3021714286, 0.7376047619,
 416     0.5711857143, 0.3481666667, 0.7424333333, 0.5472666667,
 417     0.3952571429, 0.7459, 0.5244428571, 0.4420095238, 0.7480809524,
 418     0.5033142857, 0.4871238095, 0.7490619048, 0.4839761905,
 419     0.5300285714, 0.7491142857, 0.4661142857, 0.5708571429, 0.7485190476,
 420     0.4493904762, 0.609852381, 0.7473142857, 0.4336857143,
 421     0.6473, 0.7456, 0.4188, 0.6834190476, 0.7434761905, 0.4044333333,
 422     0.7184095238, 0.7411333333, 0.3904761905,
 423     0.7524857143, 0.7384, 0.3768142857, 0.7858428571, 0.7355666667,
 424     0.3632714286, 0.8185047619, 0.7327333333, 0.3497904762,
 425     0.8506571429, 0.7299, 0.3360285714, 0.8824333333, 0.7274333333, 0.3217,
 426     0.9139333333, 0.7257857143, 0.3062761905, 0.9449571429, 0.7261142857,
 427     0.2886428571, 0.9738952381, 0.7313952381, 0.266647619,
 428     0.9937714286, 0.7454571429, 0.240347619, 0.9990428571, 0.7653142857,
 429     0.2164142857, 0.9955333333, 0.7860571429, 0.196652381,
 430     0.988, 0.8066, 0.1793666667, 0.9788571429, 0.8271428571, 0.1633142857,
 431     0.9697, 0.8481380952, 0.147452381, 0.9625857143, 0.8705142857, 0.1309,
 432     0.9588714286, 0.8949, 0.1132428571, 0.9598238095, 0.9218333333,
 433     0.0948380952, 0.9661, 0.9514428571, 0.0755333333,
 434     0.9763, 0.9831, 0.0538
 435 ]
 436 
 437 
 438 def beta(x, y):
 439     return gamma(x) * gamma(y) / gamma(x + y)
 440 
 441 def clamp(x, lowest, highest):
 442     '''
 443     Keep the first number given between the other two. The second number
 444     is supposed to be less than or equal to the third number given.
 445     '''
 446     return max(lowest, min(x, highest))
 447 
 448 def deinf(x, default):
 449     '''Replace either kind of floating-point infinity with another value.'''
 450     return x if not isinf(x) else default
 451 
 452 def denan(x, default):
 453     '''Replace a floating-point NaN with an alternative value.'''
 454     return x if not isinf(x) else default
 455 
 456 def lbeta(x, y):
 457     return lgamma(x) + lgamma(y) - lgamma(x + y)
 458 
 459 def log(x, base = e):
 460     try:
 461         return math_log(x, base)
 462     except Exception as _:
 463         return nan
 464 
 465 def osc(x):
 466     '''
 467     This `oscillator` func is just a handy shortcut to calculate the sine,
 468     after multiplying the argument by 2 * pi.
 469     '''
 470     return sin(tau * x)
 471 
 472 def revalue(x, default):
 473     '''Replace floating-point NaNs and infinities with another value.'''
 474     return x if not (isinf(x) or isnan(x)) else default
 475 
 476 def scale(x, x0, x1, y0, y1):
 477     return ((x - x0) / (x1 - x0)) * (y1 - y0) + y0
 478 
 479 def sign(x):
 480     if isnan(x):
 481         return nan
 482     return +1.0 if x > 0.0 else (-1.0 if x < 0.0 else 0.0)
 483 
 484 sgn = sign
 485 
 486 def sinc(x):
 487     return sin(x) / x if x != 0.0 else 1.0
 488 
 489 def sqrt(x):
 490     try:
 491         return math_sqrt(x)
 492     except Exception as _:
 493         return nan
 494 
 495 def wrap(x, y0, y1):
 496     return x * (y1 - y0) + y0
 497 
 498 
 499 def write_bmp(w, grid, low, high, width, height):
 500     dib_size = 40
 501     hdr_size = 14 + dib_size
 502     img_size = 3 * width * height
 503     w = w.write
 504 
 505     w(b'BM')
 506     w(pack('<I', hdr_size + img_size))
 507     w(pack('<H', 0))
 508     w(pack('<H', 0))
 509     w(pack('<I', hdr_size))
 510     w(pack('<I', dib_size))
 511     w(pack('<i', width))
 512     w(pack('<i', height))
 513 
 514     w(pack('<H', 1)) # 1 color plane
 515     w(pack('<H', 24)) # 24 bits per pixel
 516     w(pack('<I', 0)) # no compression
 517     w(pack('<I', img_size)) # number of bytes for the pixels
 518     w(pack('<i', 0)) # horizonal and vertical pixels/m
 519     w(pack('<i', 0)) # 2**n palette colors
 520     w(pack('<I', 0)) # all colors are important
 521     w(pack('<I', 0))
 522 
 523     span = high - low
 524     bad = b'\x00\x00\xff' # color-channel order seems to be BGR, not RGB
 525     bgr = bytearray(b'\x00\x00\x00')
 526 
 527     palette = viridis
 528     # palette = parula
 529     k = float(len(palette) / 3 - 1)
 530 
 531     for line in grid:
 532         for z in line:
 533             if isnan(z) or isinf(z):
 534                 w(bad)
 535                 continue
 536             v = int(round(k * (z - low) / span))
 537             i = 3 * v
 538             bgr[2] = int(255.0 * palette[i + 0])
 539             bgr[1] = int(255.0 * palette[i + 1])
 540             bgr[0] = int(255.0 * palette[i + 2])
 541             w(bgr)
 542 
 543 
 544 def parse_range(s):
 545     pieces = s.split('..')
 546     if len(pieces) == 2:
 547         try:
 548             return float(pieces[0]), float(pieces[1])
 549         except Exception as _:
 550             pass
 551     return nan, nan
 552 
 553 
 554 square_output = False
 555 hd_opts = ('-hd', '--hd', '-1080p', '--1080p')
 556 square_opts = ('-s', '--s', '-square', '--square', '-squared', '--squared')
 557 
 558 src = ''
 559 width = 1920 * 2
 560 height = 1080 * 2
 561 xmin = 0.0
 562 xmax = 1.0
 563 ymin = 0.0
 564 ymax = 1.0
 565 
 566 args = argv[1:]
 567 
 568 while len(args) > 0:
 569     if args[0] == '-':
 570         break
 571 
 572     if args[0] == '--':
 573         args = args[1:]
 574         break
 575 
 576     if args[0] in hd_opts:
 577         width = 1920
 578         height = 1080
 579         args = args[1:]
 580         continue
 581 
 582     if args[0] in square_opts:
 583         square_output = True
 584         args = args[1:]
 585         continue
 586 
 587     if args[0].startswith('-xy=') or args[0].startswith('--xy='):
 588         x, y = parse_range(args[0][args[0].index('=') + 1:])
 589         if isnan(x):
 590             break
 591         args = args[1:]
 592         xmin = ymin = x
 593         xmax = ymax = y
 594         continue
 595 
 596     if args[0].startswith('-x=') or args[0].startswith('--x='):
 597         x, y = parse_range(args[0][args[0].index('=') + 1:])
 598         if isnan(x):
 599             break
 600         args = args[1:]
 601         xmin = x
 602         xmax = y
 603         continue
 604 
 605     if args[0].startswith('-y=') or args[0].startswith('--y='):
 606         x, y = parse_range(args[0][args[0].index('=') + 1:])
 607         if isnan(x):
 608             break
 609         args = args[1:]
 610         ymin = x
 611         ymax = y
 612         continue
 613 
 614     if args[0].startswith('-'):
 615         from sys import stderr
 616         print(f'fh: unsupported option {args[0]}', file=stderr)
 617         exit(1)
 618 
 619     break
 620 
 621 if square_output:
 622     width = height
 623 
 624 if len(args) != 1:
 625     from sys import exit, stderr
 626     print(info.strip(), file=stderr)
 627     exit(1)
 628 
 629 
 630 x = 0.0
 631 y = 0.0
 632 prev = 0.0
 633 if xmax < xmin:
 634     xmin, xmax = xmax, xmin
 635 if ymax < ymin:
 636     ymin, ymax = ymax, ymin
 637 dx = (xmax - xmin) / (width - 1.0)
 638 dy = (ymax - ymin) / (height - 1.0)
 639 w = width
 640 h = height
 641 low = +inf
 642 high = -inf
 643 
 644 src = args[0]
 645 grid = [[0.0] * width for _ in range(height)]
 646 dest = stdout.buffer
 647 
 648 try:
 649     # giving func eval a compiled string considerably speeds things up
 650     expr = compile(src, '<string>', 'eval')
 651     # prevent expression from using some global values
 652     compile = exec = exit = open = stdout = None
 653 
 654     for j in range(height):
 655         row = grid[j]
 656         y = float(height - 1 - j) * dy + ymin
 657 
 658         for i in range(width):
 659             x = float(i) * dx + xmin
 660             prev = eval(expr)
 661             row[i] = prev
 662 
 663             if isinf(prev) or isnan(prev):
 664                 continue
 665 
 666             low = min(low, prev)
 667             high = max(high, prev)
 668 
 669     if isnan(low) or isnan(high):
 670         msg = 'function is completely undefined in the sampled (x, y) region'
 671         raise ValueError(msg)
 672     if isinf(low) or isinf(high):
 673         msg = 'function spans an infinite range in the sampled (x, y) region'
 674         raise ValueError(msg)
 675     write_bmp(dest, grid, low, high, width, height)
 676 except KeyboardInterrupt:
 677     from sys import exit
 678     exit(2)
 679 except OSError as _:
 680     # quit quietly, instead of showing a confusing error message
 681     from sys import exit, stderr
 682     stderr.close()
 683     exit(0)
 684 except Exception as e:
 685     # raise e
 686     from sys import exit, stderr
 687     print(str(e), file=stderr)
 688     exit(1)