Dividing 0 to 100% exponetially for dimming light - Page 1
Forum section: electronics / beginners

author: Vindhyachal.takniki
author_rank: Frequent Contributor
timestamp: February 13, 2021, 03:26:58 pm
content: 1. I need to dim a light 0 to 100% depending upon user input 0 to 100.Have a driver to which when I pass value 0 to 100% by microcontroller it dims the led accordingly.2. I was reading below image from this datasheet:https://www.ti.com/lit/pdf/slvaef33.Page 6, says "Human perception of brightness is not linear with luminance of the LEDs and is more sensitive at lowluminance, which is approximated to a logarithmic curve. So if the linear dimming control method is used,the final perceived brightness is not linear in human perception. That is why exponential dimming is used."4. Now have to divide 0 to 100% exponetially? How do I divide the number 0 to 100% exponentially when user input comes 0 to 100?

author: tooki
author_rank: Super Contributor
timestamp: February 13, 2021, 03:53:10 pm
content: 1. Take your input and convert it to a floating point number from 0.0 to 1.0. We’ll call this x.2. Take x and give it an exponent greater than 1: xn. The size of n will dictate how strong the exponential effect is. For example, if you use n=2, then an input of 1 gives an output of 1 (1x1=1), but an input of 0.5 gives an output of 0.25 (0.5x0.5=0.25), etc. Let’s call this result y.3. Multiply y by 100 to make it into 0-100.Note that the part of the application note you quoted is talking about how most LED drivers have exponential dimming built in, so you just tell the driver whether you want to enable exponential dimming or not. So you feed it the uncorrected values snd it does the conversion for you.

author: Zoli
author_rank: Frequent Contributor
timestamp: February 13, 2021, 04:00:41 pm
content: 4. The main question is - how many steps of luminosity are you using(PWM resolution)?If you have an answer to that, it's easy to make a geometrical progression which will fulfill your requirements.

author: tooki
author_rank: Super Contributor
timestamp: February 13, 2021, 04:09:48 pm
content: Quote from: Terry Bites on February 13, 2021, 04:07:44 pmThat table would do the exact opposite: crushing resolution at the important low end, and maximizing it at the high end where our eyes are intensive.

author: JackJones
author_rank: Regular Contributor
timestamp: February 13, 2021, 04:23:22 pm
content: https://jared.geek.nz/2013/feb/linear-led-pwm

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: February 13, 2021, 04:58:31 pm
content: Quote from: tooki on February 13, 2021, 03:53:10 pmThere is no reason whatsoever to use floating point for this.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: February 13, 2021, 05:03:11 pm
content: Squaring the value gives a pretty good approximation, scale to whatver your PWM resolution is.You need between 10 and 12 bits of hardware resolution for the smoothest fading performance.e.g. for 12 bits, square the incoming 0-100% and scale the 0..10000 figure this gives to 0..4095. Using integer maths, multiply by 104 (4095/10000*256) and divide by 256Use a lookup table for maximum flexibility.

author: CatalinaWOW
author_rank: Super Contributor
timestamp: February 13, 2021, 06:07:28 pm
content: You may prefer a non-maths way of figuring this out that also gives you a better feel for what is going on.Write yourself a small program that starts with zero brightness, and then alternates between that and a second value, initially 1. You will want to dwell on this value for a significant time, a second or so is a good start but this is also a parameter you should play with to get a full understanding. Have the program increment the second value after a few switches and provide a method for you to stop the incrementing. You will stop the program and note the second value when you see a noticeable change in brightness.Then have the program repeat using this new value as the base and incrementing. Continue until you reach your 100 steps.You will find your stopping points much closer together at the low end of the scale. If you plot them you will see a relationship that isn't a bad fit to logarithmic or several other choices. You can use this data in a look up table to generate your outputs, although for a very smooth apparent growth you will probably have to add a point between your marked points. It should be obvious why.

author: pwlps
author_rank: Frequent Contributor
timestamp: February 13, 2021, 07:18:35 pm
content: Quote from: tooki on February 13, 2021, 03:53:10 pmAs far as I understand the math here this is a power law, an exponential means x would be in the exponent. That said for such a mild exponential it probably doesn't matter much which law you use (except for the lowest values).Anyway here is a general normalized formula for an exponential transformation within the interval (0,1) :y=(exp(a*x)-1)/(exp(a)-1)where both x and y are between 0 and 1, and a is a parameter controlling how steep is the curve: for a<<1 you get a line, the curve fromhttps://www.ti.com/lit/pdf/slvaef3corresponds to an a somewhere between 4 and 6.Obviously if you work in the interval (0,100) you have y=100*(exp(a*x/100)-1)/(exp(a)-1)

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 13, 2021, 09:09:19 pm
content: More generally, the x^n form gives gamma correction, n usually being a real number gamma say 1-3 or so, with 2-2.2 being typical.If you find a more aggressive response feels better, use the exponential function above.Quote from: mikeselectricstuff on February 13, 2021, 04:58:31 pmEh, it's handy. If nothing else, it represents the process to be implemented in fixed point. In particular, it's always a ratio 0..1, so the point is also always fixed. Easy.Taken literally, the cost in compute time or code space depends on the platform, which is undefined. On embedded devices, it is usually best avoided.Tim

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: February 13, 2021, 10:53:03 pm
content: Quote from: Terry Bites on February 13, 2021, 10:49:11 pmLess sensitive

author: DrG
author_rank: Super Contributor
timestamp: February 14, 2021, 12:50:25 am
content: Quote from: Terry Bites on February 13, 2021, 10:49:11 pmAs already noted, less sensitive [to change]. Psychophysics in general is fascinating. Remember, "brightness" as it is being used here, represents your perception - how bright you say the stimulus is with various levels of stimulus intensity (usually luminence here, but generalized to duty-cycle and so on). There are a few ways of running these kinds of experiments (see:http://www.cns.nyu.edu/~msl/courses/0022X/slides/sl-psychophysics.pdf).The relationship of physical intensity to perceived magnitude looks like a power function across a lot of modalities, not just light. Hence the popularity of Steven's power law (superseding Weber-Fechner ).In an earlier post, reference [https://jared.geek.nz/2013/feb/linear-led-pwm] was made to the CIE 1931 work], and that usually gets cited by EE nerds...and it should be.There are some caveats because this is the CNS and there are always caveats and special situations and methodology limitations.https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htmSo, basically, the programmer's chore, with regard to the OP's dimming need, is to curve-fit that function (usually broken up into two function) and and go from there with your PWM duty cycle values. In reasonable practice for what the OP wants to do, I agree that a table is the way to go, but there is already enough in the thread for different ways of accomplishing the task.Interestingly, the OP's TI reference (Fig 10) shows the two ways but says "The user can choose the one that best fits the system." I say "interestingly" because the straight linear way usually sucks - I mean, that is the whole point of going to the trouble, but yes, either way you are dimming.Two points that are kind of interesting that I would like to add are:We conveniently expect such a power relationship based on excitation of rods and cones, but there may be some other photosensitive elements/componentshttps://www.nature.com/articles/s41598-019-44035-3When I have futzed around with this stuff (like with color fading), I found that LEDs have, what I would call a threshold duty cycle. That is, very small duty cycles are not discriminable from duty cycle=0. Thus, while maintaining a duty cycle=0 in the array, I would dispense with duty cycles that were below threshold (I don't mean measurable as physical entities but that normal people could not tell that anything was going on with the LED). So, I would "fudge" the array so that the first non-zero duty cycle was the lowest perceptible intensity. The rationale is that I see no point in having multiple "off" values. I don't know if LEDs have some minimal power requirement to "excite", I would guess that they do, but I don't know how to calculate it other than empirically.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 14, 2021, 01:48:04 am
content: With such a small number of possible inputs (101 values; from 0 to 100, inclusive), a look-up table for the PWM duty cycle sounds the best option.To generate the tables, one can use$$P(x) = P_\min + (P_\max - P_\min) \, \left(\frac{x}{x_\max}\right)^g$$where \$P_\min\$ is the minimum PWM duty cycle value described by DrG, \$P_\max\$ is the maximum duty cycle value, and \$x_\max\$ is the maximum input value (that produces the maximum duty cycle value \$P_\max\$). You use \$0\$ for \$x = 0\$, but \$P(x)\$ for \$0 \lt x \le x_\max\$; and \$P(x_\max) = P_\max\$.The exponent \$g\$ is the reciprocal of the sensitivity exponent (Stevens's power law, describing common psychophysical exponents \$a\$; \$g = 1/a\$). As mentioned before, \$g\$ is typically somewhere around 2 (as \$a\$ is somewhere around \$0.5\$).It may be interesting to note that the exponent \$g\$ actually caters to two different things: one is psychophysical, and the other is the nonlinearity in the electronics/mechanics. For example, if we were controlling current and not PWM duty cycle, we'd have to take into account that LED luminosity at half the current is much higher (than half), so we'd use a larger exponent \$g\$, say around 3 - 4.

author: Renate
author_rank: Super Contributor
timestamp: February 14, 2021, 12:55:55 pm
content: Why 0 to 100?Usually you want as much resolution as possible without jumping to a bigger data size.All my little MCU projects use a heartbeat LED ramping up and down slowly.The conversion is by a 256 byte table.At the "bright" end of the table the output jumps many output values for an input change of 1.At the "dim" end of the table the output sits on the same value for a few changes of the input.Still, 8 bits in, 8 bits out is plenty of resolution.

author: Vindhyachal.takniki
author_rank: Frequent Contributor
timestamp: February 14, 2021, 02:48:42 pm
content: 1. I am reading this site:https://jared.geek.nz/2013/feb/linear-led-pwmIt has below formula:Attached is excel for it also/2. But I think instead of 119 on line 2 in formula , it should be 116, otherwise at 100% input, I dont get value of 100.

author: JackJones
author_rank: Regular Contributor
timestamp: February 14, 2021, 03:41:50 pm
content: Someone commented about that on the site as well. I have an old version of the script stored, and it is correct at 116. No idea why it has changed since.It is also missing f.close()

author: DrG
author_rank: Super Contributor
timestamp: February 14, 2021, 04:02:03 pm
content: Quote from: Vindhyachal.takniki on February 14, 2021, 02:48:42 pmI didn't look at that article real carefully, but it looks like the author changed the formulas based on a research report. Unfortunately a working link to that report is not there.If you look here:https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htmYou see the formulas as:Code:Y = (L* / 902.3) ∙ Yn L* ≤ 8Y = ((L* + 16) / 116)3 * Yn L* > 8Also: If you do implement this, I would like to know the first duty cycle value that produces any visible light. That is, I am guessing that (at least) the first 10 values that the user enters (1-11) are functionally equivalent to 0. So, the question that I raised early might apply: Why have multiple 'off' values?

author: Renate
author_rank: Super Contributor
timestamp: February 14, 2021, 04:38:39 pm
content: Quote from: DrG on February 14, 2021, 04:02:03 pmBecause if the input is a triangle wave (counting up to 255 then down to 0 at a regular rate) you need all those zeroes so that it looks good to the eye.If there was only one zero you would say that it never went off entirely.And usually we are working at a constant rate.The other way to do it is too painful and brings us nothing.We could start at brightness 0x00 then wait 7.635 mS (or whatever) before changing to 0x01 output.When we got up to bright we would have to wait 0.296 mSec (or whatever) to change from 0xfe to 0xff.(Those number are somewhere on the right scale.)It isn't worth setting a 100 kHz timer for a blinky.

author: DrG
author_rank: Super Contributor
timestamp: February 14, 2021, 04:58:10 pm
content: Quote from: Renate on February 14, 2021, 04:38:39 pmDifferences of opinion are fine. A user inputs a dim value between 0-100. Why have dim values > 0 that are functionally the same as 0? It is a rhetorical question as my point is that you don't need them and, in there place can be finer adjustments (or fewer adjustments, if desired). They waste space and function.

author: Renate
author_rank: Super Contributor
timestamp: February 14, 2021, 06:05:47 pm
content: Quote from: DrG on February 14, 2021, 04:58:10 pmMmm, ok. Then trim down the number of user choices to the point that they are distinct and logarithmically evenly spaced.Or use more output bits, some combination of PWM and current.Or an 8 bit DAC to an exponential driver.

author: DrG
author_rank: Super Contributor
timestamp: February 14, 2021, 07:00:23 pm
content: Quote from: Renate on February 14, 2021, 06:05:47 pmWhat problem are you solving? I ask rhetorically, because I already said eliminate non-visible stimulus intensities and would extend that to non-discriminable intensity changes - that was my point. That is how I solve that problem, but I respect that you don't like that approach and that you might solve it differently or offer other ways to solve that problem. I might even agree that you can propose a better solution, if you so demonstrate a better solution - but I think you understand my point and there is no reason to argue or be contentious about it - so I am done with that part of the discussion with you. What I am getting at is a JND and I will address that in another post so it does not look like it is just bickering between us.

author: DrG
author_rank: Super Contributor
timestamp: February 14, 2021, 08:08:38 pm
content: So, I think the OP has his answer and his array for 0-100, but the exercise had me thinking and playing around a bit (because it is fun for me). These are just some thoughts and not of the "I'm right so you are wrong" variety. In fact, I welcome some other POVsI reworked the OP's spreadsheet and it is attached. Here is the function.In the spreadsheet, I added columns for the int DC value for 8 and 16 bit resolution.For the 8-bit resolution, look at the low range (and remember, from the CIE 1931, the low range ( < 8 ) is typically broken out as was done in the spreadsheet calculation formulas and sorry about the lack of formatting).First off, 13/22 entries can be eliminated because they are redundant. I note though that it might simply be convenient leave them in the array as a programming matter. There are no redundant values in the 16-bit column.Next, I wanted to at least look at how the idea of a JND (just noticeable difference) figures into this. I hooked up a cheapo LED with a 150 ohm resistor to an Arduino UNO port (normally, I would have used a higher resistor value; 220 or 330, but I wanted to see what I could at low PWM duty cycles biased for that low end). The two programs (one for 8-bit resolution and one for 16-bit resolution are attached as .txt files). These simple programs simply go between two duty cycle values.For an 8-bit DC resolution, a value of 1 is easily discriminable from 0. I could not however discriminate between DC values of 37 from 38 (as show up on the spreadsheet).For a 16-bit DC resolution, the lowest spreadsheet value of 72 is easily discriminable from 0. Values of ~1-5 are not discriminable (from 0) even with a 7X loop. Values~ 5-12 are visible with a loop but not discriminable from 0 at any practical distance. Notably, a value of 36 is discriminable from 72 (which, again, is the lowest on value in the spreadsheet) - so, it is theoretically meaningful to start that array at a lower value. A value of 4359 is not discrimnable from 4643 (adjacent values in the spreadsheet).So, this gets me thinking about what the 'optimal' dimming array could be. My thinking is that the optimal array would have all JND values between 0 and max, and of course that is going to depend on PWM resolution [and other things]. Not (yet) sure how that gets calculated, but I bet it has been done. Edited to add: This looks promissing but I need some time (and motivation) to digest ithttps://www.researchgate.net/publication/305319583_Dimming_curve_based_on_the_detectability_and_acceptability_of_illuminance_differencesAs far as the OP's exercise: To me, 100 values is definitely more than is needed or will be useful. I get it, if the assignment was that the user inputs a value between 0-100 - so be it.That's all I got right now and sorry about any stupid errors

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 14, 2021, 10:29:28 pm
content: The above few posts refer to CIE 1931, whose inverse function (which we could use here) is given as an approximation. Using \$x_\max\$ for the maximum input/control value, \$P_\max\$ for the maximum duty cycle value, and$$\begin{aligned}x_\min &= \frac{8}{100} x_\max = 0.08 \, x_\max \\\end{aligned}$$the duty cycle is defined in two parts, one from \$0\$ to \$x_\min\$, and the other from \$x_\min\$ to \$x_\max\$, theexactinverse would be$$P = \begin{cases}\displaystyle P_\max \frac{2700}{29^3} \frac{x}{x_\max}, & 0 \le x \le x_\min \\\displaystyle P_\max \left( \frac{4^3}{29^3} + \frac{1200}{29^3} \left(\frac{x}{x_\max}\right) + \frac{1200}{29^3} \left(\frac{x}{x_\max}\right)^2 + \frac{7500}{29^3} \left(\frac{x}{x_\max}\right)^3 \right), & x_\min \le x \le x_\max \\\end{cases}$$but since the inverse is approximated using a cubic "monomial" (no first or second degree terms), the exact cubic monomial specified in CIE 1931 would be$$P = \begin{cases}\displaystyle P_\max \frac{2700}{29^3} \frac{x}{x_\max}, & 0 \le x \le x_\min \\\displaystyle P_\max \left( \frac{1051 \cdot 25^3}{679 \cdot 29^3} \left( \frac{x}{x_\max} \right)^3 + \frac{138256}{679 \cdot 29^3} \right), & x_\min \le x \le x_\max \\\end{cases}$$or approximately$$P = \begin{cases}\displaystyle P_\max \, 0.110705646 \frac{x}{x_\max}, & 0 \le x \le x_\min \\\displaystyle P_\max \left( (1 - 0.00834872623) \left(\frac{x}{x_\max}\right)^3 + 0.00834872623 \right), & x_\min \le x \le x_\max \\\end{cases} \tag{*}\label{NA}$$The error in using such a cubic "monomial" is largest at around \$x \approx 0.655383 \, x_\max\$, where the exact cubic inverse yields \$P = 0.3473 \, P_\max\$, but the CIE 1931 monomial \$P = 0.2875 \, P_\max\$.In other words, at about two thirds luminance, the CIE 1931 standard fudges the inverse (the needed PWM duty cycle to achieve the desired luminosity) by about 6%. That's acceptable, because there is that much leeway in human perception.A similar issue occurs with audio level potentiometers. They too should follow a power law, loudness having \$a \approx 0.67\$ so the comparable exponent here would be \$g = 1/0.67 \approx 1.5\$, or \$g = 3/2\$. Instead of any sort of an exponential or power curve, they're implemented by just two linear parts. We humans are just not that fussy if each bar or tick on the control varies by ±10% or even ±20% of the output, depending on the sensorium used.Therefore, I'd say the best option is to use a precalculated table based on \$\eqref{NA}\$, or the equation directly using e.g integer math.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 14, 2021, 10:45:47 pm
content: On a related note, some time ago, I wrote a (compile-time) variable gamma function for embedded systems.Code:uint16_t gammaCorrection(uint8_t g) {#define GAMMA_EXPONENT2.2#define GAMMA_P0(g)( (((((-1288 * (g) + 15997) * (g) + -80995) * (g) + 210128) * (g) + -279795) * (g) + 152320) * (g) )#define GAMMA_P1(g)( ((((( 5371 * (g) + -66917) * (g) + 335766) * (g) + -841258) * (g) + 1026871) * (g) + -459709) * (g) )#define GAMMA_P2(g)( (((((-7607 * (g) + 94355) * (g) + -467535) * (g) + 1138966) * (g) + -1323332) * (g) + 564949) * (g) )#define GAMMA_P3(g)( ((((( 3985 * (g) + -48101) * (g) + 231174) * (g) + -543075) * (g) + 608860) * (g) + -252779) * (g) )#define GAMMA_P4(g)( 0 * (g) )uint16_t r;r = ((int32_t)GAMMA_P4(GAMMA_EXPONENT) * g) >> 8;r += ((int32_t)GAMMA_P3(GAMMA_EXPONENT) * g) >> 8;r += ((int32_t)GAMMA_P2(GAMMA_EXPONENT) * g) >> 8;r += ((int32_t)GAMMA_P1(GAMMA_EXPONENT) * g) >> 8;r += ((int32_t)GAMMA_P0(GAMMA_EXPONENT) * g) >> 8;return ((int32_t)r * 10000) >> 14;}I think this is 8.24 fixed format internally, and the return expression is just to normalize it to a 10k output range? Been a little while since I touched this codebase.This works by fitting a, cubic polynomial I guess I found the best accuracy with?, to a range of powers. This is actually a double fit, where the coefficients for one best-fit (the actual gamma correction function being evaluated) are generated by additional best-fitting -- hence the matrix of coefficients represented by the GAMMA_Pn defines. These are evaluated at compile-time to give the coefficients for the selected exponent.Polynomials are great for platforms with hardware multiplication -- the whole thing might only take hundreds of CPU cycles to compute, versus, I don't know, thousands with floating point pow()? (Less still with hand optimization -- this was on AVR, which GCC is notoriously bad at optimizing. Also, some code space can be saved putting the constants into PROGMEM, as I've done in the past for a thermistor correction function:https://www.seventransistorlabs.com/Calc/Thermistor.html(towards the bottom, oh dear, I never put a table of contents on this page, for shame!))Tim

author: DrG
author_rank: Super Contributor
timestamp: February 14, 2021, 11:03:16 pm
content: Quote from: Nominal Animal on February 14, 2021, 10:29:28 pmSo given your appreciation for psychophysics as well as your mathematicalproclivityskill, let me ask you:How would you (or really anyone)systematicallyfill a 20 [or 21, including 0] element array with optimal dimming values (as PWM Duty Cycle, i.e., steps) with a min=0 and a max=100%. For the sake of illustration, assume an 8-bit DC and at 5V.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 15, 2021, 12:19:05 am
content: In terms of optimality, do you want an error bound for that as well?Put another way: say we want a 20-position attenuator switch, using off-the-shelf components, so, their values are quantized -- which is not quite the same problem, but it is if we force the assumption that we can only make integral fractions of 256 with available resistors.The limiting factor will be the first 1/20th step, expressed in 8 bits. So the error is easily calculated from that, and some decision on which way rounding should fall (this will depend on gamma, or whatever function is chosen -- a half-LSB has different weight up/down), and whether it's better to have any change at all (e.g., forcing the first few steps to be a linear sequence, just so there's some difference between them, even if it's worse error overall).Even with those considerations, fortunately it's not a nasty problem, probably the simplest solution is very close to adequate: start with the array of values [0...1), pass them through the function, and round to 1/256th.Tim

author: DrG
author_rank: Super Contributor
timestamp: February 15, 2021, 12:47:18 am
content: Quote from: T3sl4co1l on February 15, 2021, 12:19:05 amThis is how I have done it and, quite frankly, I don't know a better way.Using the formulas present in the spreadsheets, I generated 20 values and a 0 value/The resulting array is:Code:dimming = {0, 1, 2, 4, 7, 11, 15, 21, 28, 37, 46, 58, 71, 86, 103, 123, 144, 168, 194, 223, 255};The graph looks reasonable (edit: when oriented correctly).ALL adjacent values represent at least one JND.Works fine on the breadboard.No problem...except when you need (for some reason) to create larger arrays that do not have adjacent values that are at least one JND. I don't have a systematic way of dealing with that situation.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 15, 2021, 03:02:08 am
content: Quote from: DrG on February 14, 2021, 11:03:16 pmUsing the following Python3 program (edited to fix a premature rounding bug):Code:#!/bin/env python3# SPDX-License-Identifier: CC0-1.0# -*- coding: utf-8 -*-if __name__ == '__main__':import argparsefrom sys import stdout, stderr, exitparser = argparse.ArgumentParser(description='Generate C code for controlling LED PWM duty')parser.add_argument('-v', '--verbose', dest='verbose', action='count', default=0, help='Verbose output')parser.add_argument('-g', '--generate', dest='generate', action='count', default=0, help='Generate C code')parser.add_argument('cmin', metavar='CMIN', type=int, help='Minimum input/control value')parser.add_argument('cmax', metavar='CMAX', type=int, help='Maximum input/control value')parser.add_argument('dmin', metavar='DMIN', type=int, help='Minimum PWM duty value')parser.add_argument('dmax', metavar='DMAX', type=int, help='Maximum PWM duty value')parser.add_argument('-e', metavar='G', type=float, dest='exp', default=3.0, help='Exponent, default 3')parser.add_argument('-c', metavar='C', type=float, dest='cmid', default=0.08, help='Initial input linear range, default 0.08')parser.add_argument('-d', metavar='D', type=float, dest='dmid', default=0.008856451679, help='Fraction of PWM range covered by linear part, default 0.0085645')args = parser.parse_args()cmin = args.cmincmax = args.cmaxif cmax - cmin < 3:stderr.write("You only have on/off control.\n")exit(1)dmin = args.dmindmax = args.dmaxg = args.expif g <= 0.0:stderr.write("Exponent must be positive.\n")exit(1)cmid = args.cmidif cmid <= 0:# Disabledcmid = 0elif cmid < 1:cmid = (((1 - cmid) * cmin) + (cmid * cmax))else:cmid = float(cmid)if cmid < cmin or cmid > cmax:stderr.write("Invalid initial linear input range.\n")exit(1)cm = round(cmid)dmid = args.dmidif dmid <= 0:# Flat or disableddmid = 0elif dmid < 1:dmid = (((1 - dmid) * dmin) + (dmid * dmax))else:dmid = float(dmid)if dmid < dmin or dmid > dmax:stderr.write("Invalid part of PWM range covered by linear input part.\n")exit(1)if args.verbose > 0:stderr.write('cmin=%d, cmid=%.3f (%d), cmax=%d, dmin=%d, dmid=%.3f, dmax=%d, g=%.6f\n' % (cmin, cmid, cm, cmax, dmin, dmid, dmax, g))table = {}for c in range(cmin, cm):d = round(dmin + (dmid - dmin) * (c - cmin) / (cmid - cmin))table = dcd = (cmax**g) - (cmid**g)c0 = dmid*(cmax**g) - dmax*(cmid**g)c1 = dmax - dmidfor c in range(cm, cmax + 1):d = round((c0 + c1*(c**g))/cd)table = d# Alternate value for the switchover pointif cm > cmin:d = round(dmin + (dmid - dmin) * (cm - cmin) / (cmid - cmin))if table != d and table[cm-1] == table:table = dif args.generate == 0 or args.verbose > 0:stderr.write(' control | duty\n')stderr.write('---------+----------\n')for c in range(cmin, cmax+1):stderr.write('%8d | %8d\n' % (c, table))if args.generate > 0:stdout.write('\nconst int pwm_lookup[%d] = {' % (cmax - cmin))columns = 8column = 0for c in range(cmin, cmax):if column <= 0 or column >= columns:stdout.write('\n %d,' % table)column = 1else:stdout.write(' %d,' % table)column += 1stdout.write('\n};\n\nstatic inline int pwm_duty(const int x) {\n')stdout.write(' if (x < %d)\n' % cmin)stdout.write(' return 0; /* Off */\n')stdout.write(' if (x < %d)\n' % cmax)if cmin != 0:stdout.write(' return pwm_lookup[x - %d];\n' % cmin)else:stdout.write(' return pwm_lookup;\n')stdout.write(' return %d; /* Full */\n' % dmax)stdout.write('}\n\n')Runpython3 pwmduty.py -hto see the usage:Code:usage: pwmduty.py [-h] [-v] [-g] [-e G] [-c C] [-d D] CMIN CMAX DMIN DMAXGenerate C code for controlling LED PWM dutypositional arguments:CMIN Minimum input/control valueCMAX Maximum input/control valueDMIN Minimum PWM duty valueDMAX Maximum PWM duty valueoptional arguments:-h, --help show this help message and exit-v, --verbose Verbose output-g, --generate Generate C code-e G Exponent, default 3-c C Initial input linear range, default 0.08-d D Fraction of PWM range covered by linear part, default0.0085645If I have a control value between 0 and 20, inclusive, and I can set the PWM duty cycle between 0 and 255, inclusive, then with CIE 1931 defaults,runpython3 pwmduty.py 0 20 0 255 -g:Code:const int pwm_lookup = {0, 1, 2, 3, 4, 6, 9, 13, 18, 25, 34, 44, 57, 72, 89, 109, 132, 157, 186, 219,};static inline int pwm_duty(const int x) {if (x < 0)return 0; /* Off */if (x < 20)return pwm_lookup;return 255; /* Full */}Let's say you are instead controlling the current, with the luminosity of the LED depending on the current by say \$L(I) \approx I^{0.75}\$. We then need to use an exponent of \$3/0.75 = 4.0\$. Let's say we get the input from a linear potentiometer, which produces ADC readings between 30 and 50, inclusive (say, after filtering). Let's say our PWM duty effective range is from 9 to 251. We runpython3 pwmduty.py 30 50 9 251 -e 4 -g:Code:const int pwm_lookup = {9, 10, 13, 20, 27, 34, 42, 51, 61, 71, 83, 95, 108, 122, 137, 153, 170, 188, 208, 229,};static inline int pwm_duty(const int x) {if (x < 30)return 0; /* Off */if (x < 50)return pwm_lookup[x - 30];return 251; /* Full */}In this case, we do not actually start at perfect darkness, but at the minimum possible luminosity, so one could say we'd need to shrink the linear region a tiny bit, but because CIE 1931 itself is not that exact, I don't think it matters at all.

author: DrG
author_rank: Super Contributor
timestamp: February 15, 2021, 03:49:12 am
content: Quote from: Nominal Animal on February 15, 2021, 03:02:08 amNice, did you write that program?They are close.Code:yours: 0, 1, 2, 3, 4, 6, 9, 13, 18, 25, 33, 44, 56, 71, 89, 109, 131, 157, 186, 219, 255mine: 0, 1, 2, 4, 7, 11, 15, 21, 28, 37, 46, 58, 71, 86, 103, 123, 144, 168, 194, 223, 255

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 15, 2021, 05:34:58 am
content: Quote from: DrG on February 15, 2021, 03:49:12 amYes, I found it easier than explaining using English text.I don't fully trust it, though. I think I'll do a bit of math, so that the user defines the perceptual model instead, and the script does the inversion. That has better basis in physics and psychophysics – after all, it is the perceptual model that is easier to measure and describe, rather than its inverse.

author: Terry Bites
author_rank: Super Contributor
timestamp: February 15, 2021, 10:10:44 am
content: Mike is a God!

author: tooki
author_rank: Super Contributor
timestamp: February 15, 2021, 03:07:10 pm
content: Quote from: mikeselectricstuff on February 13, 2021, 04:58:31 pmAnd what reason is therenotto? It makes the math a bit more straightforward, and it’s not like the CPU power to do this is problematic these days. (And I doubt someone asking the OP’s question is an advanced enough programmer to be able to optimize code anyway, pragmatically speaking.)

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 15, 2021, 05:38:50 pm
content: Here's a much better version of the Python generator program I posted above,psychophysics.py:Code:#!/bin/env python3# SPDX-License-Identifier: CC0-1.0# -*- coding: utf-8 -*-# Y(x) = (1 - C) + C*x**a# X(y) = (1 - (1-y)/C)**(1/a)## xc = ( yc / (a + yc - a*yc ))^(1/a)# yc = a*xc^a / (1 - (1-a)*xc^a )# C = yc/a - yc + 1 = 1/(1+(a-1)*xc^a)if __name__ == '__main__':import argparsefrom sys import stderr, stdout, exitfrom math import floor, ceilparser = argparse.ArgumentParser(description="Psychophysical sublinear fitting (0 < EXP < 1)")parser.add_argument('smin', metavar='SMIN', type=float, help='Minimum setting')parser.add_argument('smax', metavar='SMAX', type=float, help='Maximum setting')parser.add_argument('dmin', metavar='DMIN', type=float, help='Minimum device control')parser.add_argument('dmax', metavar='DMAX', type=float, help='Maximum device control')parser.add_argument('-v', '--verbose', dest='verbose', action='count', default=0, help='Verbose output')parser.add_argument('-e', '--exponent', metavar='EXP', type=float, dest='exponent', default=1.0/3.0, help='Exponent, default 0.333333')parser.add_argument('-l', '--linear', metavar='F', type=float, dest='linear', default=0.08, help='Linear fraction, default 0.08')parser.add_argument('-f', '--forward', dest='forward', action='count', default=0, help='Forward conversion table')parser.add_argument('-r', '--reverse', dest='reverse', action='count', default=0, help='Reverse conversion table')parser.add_argument('-c', dest='code', action='count', default=0, help='Generate C code')parser.add_argument('-d', dest='plot', action='count', default=0, help='Display using gnuplot')args = parser.parse_args()dmin = args.dmindmax = args.dmaxif round(dmax) <= round(dmin):stderr.write('Invalid input range.\n')exit(1)smin = args.sminsmax = args.smaxif round(smax) <= round(smin):stderr.write('Invalid output range.\n')exit(1)exponent = args.exponentif exponent <= 0.0:stderr.write('Exponent must be positive (and less than 1).\n')exit(1)if exponent >= 1.0:stderr.write('Exponent must be less than 1 (and positive).\n')exit(1)slinear = args.linearif slinear < 0.0 or slinear >= 1.0:stderr.write('Linear output fraction must be between 0 and 1.\n')if slinear > 0.0:dlinear = ( slinear / (exponent + slinear - exponent*slinear))**(1/exponent)else:dlinear = 0.0C = slinear/exponent - slinear + 1def device_to_setting(d):if d <= 0:return 0elif d <= dlinear:return d*slinear/dlinearelif d < 1:return (1 - C) + C*(d**exponent)else:return 1def setting_to_device(s):if s <= 0:return 0elif s <= slinear:return s*dlinear/slinearelif s < 1:return (1 - (1 - s)/C)**(1/exponent)else:return 1if args.verbose > 0:stdout.write('exponent = %.6f\n' % exponent)stdout.write('smin = %.6f, smax = %.6f, slinear = %.6f\n' % (smin, smax, slinear))stdout.write('dmin = %.6f, dmax = %.6f, dlinear = %.6f\n' % (dmin, dmax, dlinear))if args.verbose > 1:stdout.write('C = %.9f\n' % C)ismin = round(smin)ismax = round(smax)idmin = round(dmin)idmax = round(dmax)if args.forward > 0 or args.reverse > 0:if args.forward == 1:stdout.write(' Setting | Device control\n')stdout.write(' ----------------+----------------\n')for s in range(ismin, ismax+1):d = round(dmin + (dmax - dmin) * setting_to_device( (s - smin) / (smax - smin) ))stdout.write(' %11d %11d\n' % (s, d))stdout.write('\n')elif args.forward == 2:stdout.write(' Setting | Device control\n')stdout.write(' ----------------+----------------\n')for s in range(ismin, ismax+1):d = dmin + (dmax - dmin) * setting_to_device( (s - smin) / (smax - smin) )stdout.write(' %11d %15.4f\n' % (s, d))stdout.write('\n')if args.reverse == 1:stdout.write(' Device control | Setting\n')stdout.write(' ----------------+----------------\n')for d in range(idmin, idmax+1):s = round(smin + (smax - smin) * device_to_setting( (d - dmin) / (dmax - dmin) ))stdout.write(' %11d %11d\n' % (d, s))stdout.write('\n')elif args.reverse == 2:stdout.write(' Device control | Setting\n')stdout.write(' ----------------+----------------\n')for d in range(idmin, idmax+1):s = smin + (smax - smin) * device_to_setting( (d - dmin) / (dmax - dmin) )stdout.write(' %11d %15.4f\n' % (d, s))stdout.write('\n')if args.code:stdout.write('\nconst int device_lookup_table[%d] = {\n ' % (ismax - ismin + 1))for s in range(ismin, ismax+1):d = round(dmin + (dmax - dmin) * setting_to_device( (s - smin) / (smax - smin) ))stdout.write(' %d,' % d)stdout.write('\n};\n\nstatic inline int device_lookup(int setting) {\n')stdout.write(' if (setting < %d)\n' % ismin)stdout.write(' return %d;\n' % round(dmin))stdout.write(' if (setting <= %d)\n' % ismax)if ismin != 0:stdout.write(' return device_lookup_table[setting - %d];\n' % ismin)else:stdout.write(' return device_lookup_table;\n')stdout.write(' return %d;\n' % round(dmax))stdout.write('}\n\n')if args.plot:import subprocessdataset = []dataset += [ '$dataset << END\n' ]for s in range(ismin, ismax+1):d = round(dmin + (dmax - dmin) * setting_to_device( (s - smin) / (smax - smin) ))dataset += [ '%.0f %.0f\n' % (d, s) ]dataset += [ 'END\n','set key bottom right\n','set ylabel "Setting"\n','set xlabel "Device Control"\n','plot $dataset u 1:2 t "Generated" w lines lc rgbcolor "#000000",',' $dataset u 1:2 notitle w points pt 1 lc rgbcolor "#000000",',' %.6f+%.6f*((x-%.6f)/%.6f)**%.6f t "x^{%.6f}" w lines lc rgbcolor "#FF0000"\n' % (smin, smax-smin, dmin, dmax-dmin, exponent, exponent),'pause mouse\n' ]subprocess.run(['gnuplot'], shell=False, encoding='UTF-8', input=''.join(dataset))Run with -h or --help to see usage:Code:usage: psychophysics.py [-h] [-e EXP] [-l F] [-f] [-r] [-c] [-d]SMIN SMAX DMIN DMAXPsychophysical sublinear fitting (0 < EXP < 1)positional arguments:SMIN Minimum settingSMAX Maximum settingDMIN Minimum device controlDMAX Maximum device controloptional arguments:-h, --help show this help message and exit-e EXP, --exponent EXPExponent, default 0.333333-l F, --linear F Linear fraction, default 0.08-f, --forward Forward conversion table-r, --reverse Reverse conversion table-c Generate C code-d Display using gnuplotRunningpython3 psychophysics.py 0 20 0 255 -cgeneratesCode:const int device_lookup_table = {0, 1, 3, 5, 8, 11, 16, 22, 29, 37, 47, 58, 72, 87, 104, 123, 145, 168, 195, 223, 255,};static inline int device_lookup(int setting) {if (setting < 0)return 0;if (setting <= 20)return device_lookup_table;return 255;}If you have Gnuplot installed, use-dto display a plot comparing the curve to one without a linear initial part.If the output is not PWM but a DAC controlling the current, and the LED luminosity is ~ current0.75, then we can use exponent 0.333·0.75=0.25. We'll still get all unique values if we stretch the initial linear region from CIE 1931 8% to 8.2%:python3 psychophysics.py 0 20 0 255 -e 0.25 -l 0.082 -c:Code:const int device_lookup_table = {0, 1, 2, 3, 4, 6, 9, 13, 18, 25, 33, 42, 54, 68, 85, 104, 127, 153, 182, 216, 255,};static inline int device_lookup(int setting) {if (setting < 0)return 0;if (setting <= 20)return device_lookup_table;return 255;}and the plot shown if you also supply-dshould look like this:Mathematically, this program uses$$\begin{aligned}S(D) &= \displaystyle (1 - C) + C \, D^a \\D(S) &= \displaystyle \left(1 - \frac{1 - S}{C}\right)^{1/a} \\C &= \displaystyle \frac{S_\text{linear}}{a} - S_\text{linear} + 1 \\~ &= \displaystyle \frac{1}{1 + (a - 1) D_\text{linear}^a } \\\end{aligned}$$where \$S\$ (Setting) and \$D\$ (Device control) are both scaled to \$[0, 1]\$, \$a\$ is the exponent, and scaled at the output stage. The program takes the linear range as a fraction of the settings range, i.e. \$S_\text{linear}\$. The above functions are \$C^1\$-continuous (both value and derivative continuous at the point where we switch from linear to exponential), and exact, and should work for most exponents \$0 \lt a \lt 1\$.

author: DrG
author_rank: Super Contributor
timestamp: February 16, 2021, 01:29:25 am
content: Quote from: Nominal Animal on February 15, 2021, 05:38:50 pmGood stuff!I was playing around with this stuff tonight. I thought about what I didn't like about filling arrays with duplicate values or values that were not discriminable from each other. Ideally, it would be nice to choose  all possible values (determined by the PWM resolution). IOW rather than an arbitrary choice of the number of values to use and THEN fill them systematically, how about if we first looked at all possible sequential values that met a a JND requirement. The old Weber JND for light intensity is given at about .079-.080 (e.g.,https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwjt7c-4nO3uAhXTFFkFHfVpBucQFjALegQIHBAC&url=http%3A%2F%2Fcourses.washington.edu%2Fpsy333%2Flecture_pdfs%2FWeek1_Day4.pdf&usg=AOvVaw2Pk2NYBoIum2NJNPx1NoEN)I can't find the full paper that gets cited but I think it is this onehttps://psycnet.apa.org/record/1971-07261-001Anyways, I went through all possible values for an 8-bit resolution and calculated the difference in L for successive values (CurrentL-LastL)/CurrentL and if it was less than .079, it was not used and I went to the next 8-bit DC value and so on. If it was greater than .079, I kept that value (and updated LastL) and then proceeded.Here is what it looks like - 68 values for 8-bitsand running it with a few different LEDs (simple program listed below if anyone wants to play).Code:int ledPin = 9;int dimming={0,1,2,3,4,5,6,7,8,9,10,11,12,14,16,18,20,22,24,26,28,30,32,35,38,41,44,47,50,53,56,59,62,65,68,72,76,80,84,88,92,96,100,104,109,114,119,124,129,134,139,145,151,157,163,169,175,182,189,196,203,210,217,225,233,241,249,255};void setup() {pinMode(ledPin, OUTPUT); // sets the pin as output}void loop() {int x;for (x = 0; x < 68; x++){analogWrite(ledPin, dimming);delay(25);}for (x = 67; x > -1; x--){analogWrite(ledPin, dimming);delay(25);}}It looks quite smooth to me. Then I started messing with the JND value...particularly with the idea of making a smaller array.Here the graph for a JND of .300 added.It certainly looks flattened out, but that is because of the ordinate values. Here it is alone with a better fitting ordinate.The simple Arduweeeeeeno code using that smaller array.Code:int ledPin = 9;int dimming={0,1,2,3,5,8,12,18,26,35,45,56,69,83,99,117,137,160,186,215,255};void setup() {pinMode(ledPin, OUTPUT); // sets the pin as output}void loop() {int x;for (x = 0; x < 21; x++){analogWrite(ledPin, dimming);delay(500);}for (x = 20; x > -1; x--){analogWrite(ledPin, dimming);delay(500);}}I think that using a JND measure is probably useful to screen values in the array. On the other hand, intended purpose rules the day [as usual]. If I were doing some color mixing or some kind of sophisticated PWM LED application, I would likely want the full blown array (with the smaller JND value) and no redundant or useless values. If I were just setting brightness with a switch or a knob, I might very well prefer the short array. So, apologies to TI for that earlier crack[as usual please forgive any blunders]

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 17, 2021, 01:50:48 am
content: Quote from: DrG on February 16, 2021, 01:29:25 amExcellent thinking.The 6% approximation inherent in the CIE 1931 itself, and how audio pots are just two linear segments instead of proper logarithmic, is testament to how much "flex" there is in the human sensorium.Humans do not get annoyed if the "step" size varies smoothly, even if it does not form any kind of law; but they do get annoyed if only a small part of the whole range is actually usable. In computers, the display brightness, or volume control bar (depending on implementations, obviously!) can be very annoying, if linear: the last 25% or so does not really change anything perceptibly, whereas the change at the bottom end, say a step from 1% to 2%, can feel almost like "doubling" the brightness or loudness.This also means that both your approach, as well as say just picking a small number of points distributed to yield even sensory response and then interpolating between them linearly, will both work, and is well grounded in psychophysics, since the result will have proper properties both in the overall scale (roughlyx=y3to within ±10% absolute error) as well as in the small scale (small adjustments having the same perceptible effect to within ±20% regardless of absolute position).Quote from: DrG on February 16, 2021, 01:29:25 amI have been thinking of testing whether pulse density modulation produces better results than PWM for lights. That is actually the one thing I might consider a Raspberry Pi Pico for.The idea is exceedingly simple: you have anN-bit unsigned integer register for each output pin. Each clock cycle, the desired density/duty/value (also anN-bit unsigned integer) is added to the register value, and the carry/overflow determines the output pin state. At the extremes, you will get a single short pulses at relatively high intervals, so it might need a clock rate comparable to PWM (which makes this uninteresting, since PWM is easier to implement).The difference to PWM is that this can be adjusted every clock cycle, and the effect is immediate.

author: DrG
author_rank: Super Contributor
timestamp: February 17, 2021, 03:55:15 am
content: @Nominal Animal Thanks - I think looking at something like a JND is probably part of the process.BTW I changed the direction of crawling for the JND stuff. Previously, I had started at the low end, which had a disadvantage of not always making it to 255 for 8-bit. I changed it to start at the high end and work downward. Using a JND=.30 (as previously reported) (BTW: I mis-labelled the graph, it is .30 not 30.0), I now get more array elements - a bit surprising.Even with a JND set to .75 (giving only 13 array elements) you get a pretty decent looking function.Fun to mess around with. FWIW if anyone wants to play, the Adruino code I have been using is below...BUT...it is justslopped togetherand I know that so please don't give me a hard time about it - I really do know how to write a decent program (sort of).Code:/*PWM_Stevens_Weber.inoRuns through all 255 possibilities for 8 bit PWM resolutionand compares current and previous L values (from CIE 1931)noting which ones represent a difference threshold above thevalue for JND*/char str_store;int saveArray;void setup() {int f;Serial.begin(9600);for (f = 0; f < 256; f++) {saveArray = -1;}}void loop() {double L = 0, lastL = 0, PWMp = 0, Idiff, JND = .30;int y, counter = 0, MAXDC = 255;// for (y = 1; y <= MAXDC; y++) {for (y = MAXDC; y > 0; y--) {PWMp = ( ((double)y / (double)MAXDC) * 100.0);if (PWMp > 8) {L = pow(((PWMp + 16.0) / 116.0), 3);}else {L = (PWMp / 903.3);}Serial.print("PMWv= ");Serial.print(y);dtostrf(PWMp, 4, 1, str_store);Serial.print(" PWMp=");Serial.print(str_store);dtostrf(L, 5, 4, str_store);Serial.print(" L=");Serial.print(str_store);// check if the difference is greater than a JNDIdiff = (L - lastL) / L;dtostrf(Idiff, 4, 3, str_store);Serial.print(" Idiff=");Serial.print(str_store);if (abs(Idiff) < JND) {Serial.println(" ***");}else {Serial.println();saveArray = y;counter++;lastL = L; // if it exceeds a JND}}Serial.println();Serial.print("Finished with ");Serial.print(counter);Serial.println(" values");// print out the good values// note that the array size is hard-codedfor (y = 255; y >= 0; y--) {if (saveArray != -1) {// for array cut and paste//Serial.print(saveArray);//Serial.print(",");// for 'x,y' graphs//Serial.print(y);//Serial.print(",");Serial.print(saveArray);Serial.println();}}while (1);}BTW: I know NOTHING about PDM.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 17, 2021, 05:34:56 am
content: (You might wish to use sayjust-noticeable-differenceinstead of JND, to not scare other members away from the discussion.)Quote from: DrG on February 17, 2021, 03:55:15 amI wonder if starting at both ends (to get both ends of the scale right), and then finding the best switchover point between the two sets, would yield consistent results?Also, it might be useful to explore (brute-force-style) the result space for small differences to the just-noticeable-difference constant; for example, if user specifies JND=0.30, then do a binary search between JND=0.295 to 0.305: whenever the two result sets differ, you do a bisection. From the results, pick the set that produces the best endpoints (or has the smallest error at the switchover points, if doing the crawl from both ends).Quote from: DrG on February 17, 2021, 03:55:15 amAlthough PDM is pretty wonky if you get into it, this is a special case of it. (Special as in simple enough for an uncle bumblefuck like me to grok.)If you treat PDM as if it was PWM, but instead of having all the "On" at one end, and all the "Off" at the other end of the period, you mix and distribute them evenly, you get this Keep-It-Simple-Stupid version of PDM.Again, the generator is simple: If your period isN, output density isD=0...N-1, and state isS, then each clock cycle you doS=(S+D)%N(where % denotes the modulo operation). If the modulo operation triggers, the output is High; otherwise the output for that clock cycle is Low. This produces exactly the abovementioned bit stream.Because the bitstream is affected immediately whenDis updated, it reacts very rapidly to control changes. If you only useD=N/L...N(L-1)/L, you can trivially prove that the longest interval between output state changes isLclock cycles; so, if you mostly use the middle range, the output is much, much easier to smooth out.(Because anN-bit register only allows maximumD, (N-1)/N, it is easier to reserve the highest bit for the output state, so thatDcan be between 0 andN, inclusive; or you can skip the summation entirely for "full on" output. But that's just an implementation detail.)I wonder if people realize that for single-variate control, PDM can be this simple. I can imagine that for e.g. power supplies, it might be superior to PWM. For LED circuits, the frequency spectrum of the generated bitstream (which has the same duty cycle as a comparable PWM output) is more spread out, so perhaps one could avoid the coil and capacitor whine with 1kHz-20kHz periods (so, say 1-10 MHz clock rate).

author: DrG
author_rank: Super Contributor
timestamp: February 17, 2021, 04:34:22 pm
content: Quote from: Nominal Animal on February 17, 2021, 05:34:56 amI think you are right on the JND - it probably better to simply call it a difference threshold or criterion.I also think you are right about starting at both ends, but don't know where to do the bisection. Intuitively, once you are out of the linear portion at the low end might be reasonable point. I should check it out.I did do a little reading on PDM - it does seem kind of interesting - please let us know if you do some "experimenting".

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 17, 2021, 06:00:00 pm
content: Quote from: DrG on February 17, 2021, 04:34:22 pmI was thinking of doing both ways all the way, then find a common point - or at least a pair of points that minimizes some error or distance criterion.

author: DrG
author_rank: Super Contributor
timestamp: February 17, 2021, 06:31:34 pm
content: Quote from: Nominal Animal on February 17, 2021, 06:00:00 pmArray index values: Difference threshold=0.30Col1: Starting low, Col2: Starting high, Col3: differenceCode:11022033054-186-2129-31812-62616-103521-144527-185634-226941-288349-349958-4111768-4913779-5816091-69186104-82215118-97248133-115149167186207230255I don't know, given the difference in array size a simple difference would not work well. Interpolating some kind of common inflection point? Probably take me several hours just to get that wrongCould a point slope comparison be used...my head is starting to hurt."I got foggin in my noggin"with apologizes to Edd "Kookie" Byrnes [could have been "smog in my noggin" - I can't find the actual quote anymore.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 17, 2021, 09:55:23 pm
content: No, consider them as ordered sets:1 2 3 5 8 12 18 26 35 45 56 69 83 99 117 137 160 186 215 248and1 2 3 4 6 9 12 16 21 27 34 41 49 58 68 79 91 104 118 133 149 167 186 207 230 255These have 1, 2, 3, 12, 186 in common. Using the initial part from the bottom up set, and final part from the top down set, and ignoring the ones that yield a duplicate of the either set, we have alternatives1 2 3 5 8 12 16 21 27 34 41 49 58 68 79 91 104 118 133 149 167 186 207 230 2551 2 3 5 8 12 18 26 35 45 56 69 83 99 117 137 160 186 207 230 255Of these, the first one is longer, so we can consider it "better". So, the merged one would be1 2 3 5 8 12 16 21 27 34 41 49 58 68 79 91 104 118 133 149 167 186 207 230 255I do not believe the discontinuity of the "derivative" is something one should worry about too much: remember, audio pots are just two linear segments, but you're hard pressed to find the inflection point using just human senses.What if there are no such matching entries? Then we'd look at approximate matches, starting with ±1. In above, 26/27, 34/35, 68/69, 117/118, but preferring the largest one (because there the error has least sensory impact).Obviously, this is just my opinion.

author: DrG
author_rank: Super Contributor
timestamp: February 17, 2021, 10:44:05 pm
content: Quote from: Nominal Animal on February 17, 2021, 09:55:23 pmBut, I am not sure how the merged array is better. How would you evaluate that?One possibility is to look at the distribution of Ldiff for each of the array elements. For example, in the short array (bottom->up), this is what the Ldiff values look like (last value in the line).Code:PWMv= 1 PWMp= 0.4 L=0.0004 Idiff=1.000PWMv= 2 PWMp= 0.8 L=0.0009 Idiff=0.500PWMv= 3 PWMp= 1.2 L=0.0013 Idiff=0.333PWMv= 5 PWMp= 2.0 L=0.0022 Idiff=0.400PWMv= 8 PWMp= 3.1 L=0.0035 Idiff=0.375PWMv= 12 PWMp= 4.7 L=0.0052 Idiff=0.333PWMv= 18 PWMp= 7.1 L=0.0078 Idiff=0.333PWMv= 26 PWMp=10.2 L=0.0115 Idiff=0.321PWMv= 35 PWMp=13.7 L=0.0168 Idiff=0.316PWMv= 45 PWMp=17.6 L=0.0244 Idiff=0.310PWMv= 56 PWMp=22.0 L=0.0350 Idiff=0.304PWMv= 69 PWMp=27.1 L=0.0511 Idiff=0.315PWMv= 83 PWMp=32.5 L=0.0733 Idiff=0.302PWMv= 99 PWMp=38.8 L=0.1056 Idiff=0.306PWMv= 117 PWMp=45.9 L=0.1518 Idiff=0.305PWMv= 137 PWMp=53.7 L=0.2172 Idiff=0.301PWMv= 160 PWMp=62.7 L=0.3128 Idiff=0.306PWMv= 186 PWMp=72.9 L=0.4507 Idiff=0.306PWMv= 215 PWMp=84.3 L=0.6467 Idiff=0.303PWMv= 248 PWMp=97.3 L=0.9307 Idiff=0.305..and for the top->down arrayCode:PMWv= 255 PWMp=100.0 L=1.0000 Idiff=1.000PMWv= 230 PWMp=90.2 L=0.7673 Idiff=-0.303PMWv= 207 PWMp=81.2 L=0.5879 Idiff=-0.305PMWv= 186 PWMp=72.9 L=0.4507 Idiff=-0.304PMWv= 167 PWMp=65.5 L=0.3467 Idiff=-0.300PMWv= 149 PWMp=58.4 L=0.2642 Idiff=-0.312PMWv= 133 PWMp=52.2 L=0.2028 Idiff=-0.302PMWv= 118 PWMp=46.3 L=0.1547 Idiff=-0.311PMWv= 104 PWMp=40.8 L=0.1173 Idiff=-0.319PMWv= 91 PWMp=35.7 L=0.0885 Idiff=-0.326PMWv= 79 PWMp=31.0 L=0.0664 Idiff=-0.332PMWv= 68 PWMp=26.7 L=0.0498 Idiff=-0.335PMWv= 58 PWMp=22.7 L=0.0373 Idiff=-0.335PMWv= 49 PWMp=19.2 L=0.0280 Idiff=-0.332PMWv= 41 PWMp=16.1 L=0.0211 Idiff=-0.323PMWv= 34 PWMp=13.3 L=0.0162 Idiff=-0.308PMWv= 27 PWMp=10.6 L=0.0120 Idiff=-0.343PMWv= 21 PWMp= 8.2 L=0.0091 Idiff=-0.320PMWv= 16 PWMp= 6.3 L=0.0069 Idiff=-0.313PMWv= 12 PWMp= 4.7 L=0.0052 Idiff=-0.333PMWv= 9 PWMp= 3.5 L=0.0039 Idiff=-0.333PMWv= 6 PWMp= 2.4 L=0.0026 Idiff=-0.500PMWv= 4 PWMp= 1.6 L=0.0017 Idiff=-0.500PMWv= 3 PWMp= 1.2 L=0.0013 Idiff=-0.333PMWv= 2 PWMp= 0.8 L=0.0009 Idiff=-0.500PMWv= 1 PWMp= 0.4 L=0.0004 Idiff=-1.000As you would expect, they are mostly very close to .3 which is the difference threshold. The largest difference values are mostly unavoidable as the DC value increments by 1.I guess what I am asking is: How would you know that the merged array is "better" - by what metric?

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 17, 2021, 11:10:23 pm
content: Quote from: DrG on February 17, 2021, 10:44:05 pmThe number of possible values.You stated initially that"Ideally, it would be nice to choose  all possible values (determined by the PWM resolution). IOW rather than an arbitrary choice of the number of values to use and THEN fill them systematically, how about if we first looked at all possible sequential values that met a a JND requirement."Consider the brightness control on your display. (Its backlight duty cycle should have this relationship, after all.) It makes sense to me – as an end user – to try and maximize the number of perceptible steps the user can choose from, without undue bias at either end.

author: DrG
author_rank: Super Contributor
timestamp: February 18, 2021, 12:24:39 am
content: Quote from: Nominal Animal on February 17, 2021, 11:10:23 pmOh yes, agreed on that. So, I showed you the values that resulted in a threshold difference (.30) FROM all possible values (1-255).To further illustrate, a portion of the output looks like this: The values with *** do NOT result in a difference threshold (from the last chosen intensity), so they don't make the array.. If you set the difference threshold to a lower value, then you will find more values that meet the requirement. If you raise the threshold, you will find fewer values. But, you are always looking at all possible values.Code:[PMWv= 43 PWMp=16.9 L=0.0227 Idiff=-0.231 ***PMWv= 42 PWMp=16.5 L=0.0219 Idiff=-0.276 ***PMWv= 41 PWMp=16.1 L=0.0211 Idiff=-0.323PMWv= 40 PWMp=15.7 L=0.0204 Idiff=-0.038 ***PMWv= 39 PWMp=15.3 L=0.0196 Idiff=-0.077 ***PMWv= 38 PWMp=14.9 L=0.0189 Idiff=-0.119 ***PMWv= 37 PWMp=14.5 L=0.0182 Idiff=-0.162 ***PMWv= 36 PWMp=14.1 L=0.0175 Idiff=-0.208 ***PMWv= 35 PWMp=13.7 L=0.0168 Idiff=-0.257 ***PMWv= 34 PWMp=13.3 L=0.0162 Idiff=-0.308PMWv= 33 PWMp=12.9 L=0.0155 Idiff=-0.041 ***PMWv= 32 PWMp=12.5 L=0.0149 Idiff=-0.085 ***PMWv= 31 PWMp=12.2 L=0.0143 Idiff=-0.131 ***PMWv= 30 PWMp=11.8 L=0.0137 Idiff=-0.179 ***PMWv= 29 PWMp=11.4 L=0.0131 Idiff=-0.231 ***PMWv= 28 PWMp=11.0 L=0.0126 Idiff=-0.285 ***PMWv= 27 PWMp=10.6 L=0.0120 Idiff=-0.343PMWv= 26 PWMp=10.2 L=0.0115 Idiff=-0.046 ***PMWv= 25 PWMp= 9.8 L=0.0110 Idiff=-0.094 ***PMWv= 24 PWMp= 9.4 L=0.0105 Idiff=-0.145 ***PMWv= 23 PWMp= 9.0 L=0.0100 Idiff=-0.200 ***PMWv= 22 PWMp= 8.6 L=0.0096 Idiff=-0.258 ***PMWv= 21 PWMp= 8.2 L=0.0091 Idiff=-0.320PMWv= 20 PWMp= 7.8 L=0.0087 Idiff=-0.050 ***PMWv= 19 PWMp= 7.5 L=0.0082 Idiff=-0.106 ***PMWv= 18 PWMp= 7.1 L=0.0078 Idiff=-0.167 ***PMWv= 17 PWMp= 6.7 L=0.0074 Idiff=-0.236 ***PMWv= 16 PWMp= 6.3 L=0.0069 Idiff=-0.313PMWv= 15 PWMp= 5.9 L=0.0065 Idiff=-0.067 ***PMWv= 14 PWMp= 5.5 L=0.0061 Idiff=-0.143 ***PMWv= 13 PWMp= 5.1 L=0.0056 Idiff=-0.231 ***PMWv= 12 PWMp= 4.7 L=0.0052 Idiff=-0.333PMWv= 11 PWMp= 4.3 L=0.0048 Idiff=-0.091 ***PMWv= 10 PWMp= 3.9 L=0.0043 Idiff=-0.200 ***PMWv= 9 PWMp= 3.5 L=0.0039 Idiff=-0.333PMWv= 8 PWMp= 3.1 L=0.0035 Idiff=-0.125 ***PMWv= 7 PWMp= 2.7 L=0.0030 Idiff=-0.286 ***PMWv= 6 PWMp= 2.4 L=0.0026 Idiff=-0.500PMWv= 5 PWMp= 2.0 L=0.0022 Idiff=-0.200 ***PMWv= 4 PWMp= 1.6 L=0.0017 Idiff=-0.500PMWv= 3 PWMp= 1.2 L=0.0013 Idiff=-0.333PMWv= 2 PWMp= 0.8 L=0.0009 Idiff=-0.500PMWv= 1 PWMp= 0.4 L=0.0004 Idiff=-1.000So, at a level produced by loading 41 into the DC, compare 41 to 39...that is not meeting the threshold difference...too similar to each other. Same with 41 to 38/37/36/35... but at 34....the difference between 41 and 34 does meet the threshold...so that (34) is chosen and now the comparison is between 34 and 33...and so on.What I am not sure I understand is how merging the up and down arrays into one array would result in a "better" array - by what metric?(sorry about the post-edits)

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 18, 2021, 01:06:13 am
content: Quote from: DrG on February 18, 2021, 12:24:39 amI understood that your algorithm has a directional bias: when building the array from bottom to top, you do not always reach the maximum; and when building the array from top to bottom, you may not reach 1 and finally 0. (Because the perception exponentais less than one, we do want the array to always have at least 0 and 1.)If that is not true, then my suggestion is worthless.If that is true, then my suggestion is a sensible way to merge the best parts of the bottom-up and top-down algorithms, in a robust/reliable manner. Bottom-up will provide a precise low-side control, top-down provides a precise high-side control, and the merge/swap-over point is chosen to maximize the number of steps in the merged set while keeping the point error (at that point) minimal.Why not use e.g the delta instead when choosing the merge point?Because the parameter space we are operating in is inherently nonlinear; for luminosity/light intensity, it'sy=x1/3. That is, the derivative at one point is not directly comparable to a derivative at another point, because we're working in a very nonlinear parameter space.Okay, but wouldn't it be better to choose a merge point where the delta to previous or next matches between the two sets?Yes, it would be, but I don't think you get consecutive pairs of matching values when generating in both directions, except at the very low end (and choosing one of those just yields the top-down generated set).Why would the larger set be better than either the bottom-up or top-down set, if those also cover the full range?No, it isn't. I'm assuming that neither the top-down or bottom-up approach alonereliablygenerates sets that always include 0, 1, andmax.So, if you do have an approach that reliably generates sets that always include 0, 1, andmax, I do not have any argument claiming that the merged set would be "better" by any metric I can think of. The merging is simply an operation that I believe (based on above arguments) can pick the best parts of the bottom-up and top-down generated sets, and always produce a set that includes 0, 1, andmax, with the largest number of perceptibly similar steps.

author: CatalinaWOW
author_rank: Super Contributor
timestamp: February 18, 2021, 01:18:58 am
content: Quote from: Nominal Animal on February 18, 2021, 01:06:13 amThis discussion is demonstrating the difference between engineering and science. An engineering solution has been reached long ago, a solution that doesn't use too many points and provides a perceptually smooth change from min to max. Maybe it isn't actually the minimum or maximum, but perceptually you can't see the difference. The intermediate points may have the same imperceptible flaws. And the solution may use a few percent more memory locations than the best solution.The science solution is now going from there to a proof of minimum necessary points while achieving canonical definitions for end points and having some minimum error between the "optimum" curve and the approximation. Nothing wrong with striving for or achieving perception if you are not paying for it when you don't want or need it. But it is important to realize which solution is appropriate for your use case.

author: DrG
author_rank: Super Contributor
timestamp: February 18, 2021, 01:20:36 am
content: Quote from: Nominal Animal on February 18, 2021, 01:06:13 amWhat you say is partially true.The low to high crawling will not hit the max value (255 in this case) - at least not always, as I have shownThe low to high crawling will always include 1 because the comparison from 0 to 1 will always be above any threshold difference setting less than 1.0 and 1.0 makes no sense.The high to low crawling will always hit the max value, because that is where it starts.The high to low crawling will always hit 0 (see above). I am not sure, but I am pretty sure that it will also hit 1 - the highest difference threshold I looked at was .75 and it hit 1.Now, to be sure, I appreciate the discussion...I appreciate that someone else is even thinking about itSo, I don't think your suggestion is worthless. I just don't know hoe to evaluate the merged array other than go backwards and calculate the difference thresholds for the merged array - but I don't know what metric would make it "better". I suppose some kind of deviation measure from the chosen threshold difference orI just don't know.

author: DrG
author_rank: Super Contributor
timestamp: February 18, 2021, 01:35:44 am
content: Quote from: Nominal Animal on February 18, 2021, 01:06:13 amI think that the bottom to top approach is flawed. I favor the top to bottom approach. I think you will always hit Max, 0, and 1. I am not 100% sure about hitting 1, but working with 8 bit I think you always will. If I knew more, I could probably tell you better.If you work with a 16 bits or more, I still think that 0 and Max should be in there, but I don't know about 1.Again, I really do appreciate the discussion.

author: DrG
author_rank: Super Contributor
timestamp: February 18, 2021, 01:48:12 am
content: Quote from: CatalinaWOW on February 18, 2021, 01:18:58 amI think I get the flavor of what you are saying and I believe you have a point. The field of psychophysics started as a melding between math/physics and psychology (IMO). So, I would hope that engineering and science can, in general, be co-dependentsIf you think back to the thread origin, remember, the OP is "TOLD" that the user enters a number (0-100) to set brightness. He dutifully looked into it and noted that TI (in their app note) showed a linear way and an exponential way and left it up to the engineer to choose based on need/suitability.So, I am thinking wtf? Not only does the choice of the range seem arbitrary, but so too is the choice of intensity levels. I just can't imagine messing with a backlight on a 2X16 display with 100 levels of brightness and I have to enter that number into the program...Yes, fitness for purpose always rules, as does cost (not just $$ but ergonomics and so on). On that basis, it is probably a good idea to at least learn (for my own edification) some different ways of doing things. In practice, knowing the possible ways of doing it has a lot to being able to choose the best solution in different applications....plus, it is fun to think

author: CatalinaWOW
author_rank: Super Contributor
timestamp: February 18, 2021, 02:14:13 am
content: Quote from: DrG on February 18, 2021, 01:48:12 amYeah, I may not have stated it very clearly, but that is the direction I was going. The OP had a problem given to him. Might have been a teacher or a supervisor. And had a lot of learning to do. Learning that the eye response is non-linear was a good start. Learning the source and necessity for the input requirements and the output usage would have been good also.It is always good to learn new ways to look at problems, and new solutions to them. The bigger and better your bag of tricks is the more problems you can solve and the more efficient you can be in solving them.But in a competitive marketplace (and most surroundings are competitive in one way or another, whether it is academia, commercial work or just bragging rights at the local club) it is usually important to use the least resources (time, BOM, whatever) to achieve the desired results. But in some fields perfection is the desired result. Pure science is an example. The same rules apply, just a different scorecard.If you are in a uniform environment you can usually do the right thing on autopilot. But there are many situations where you move from one end of the spectrum to another and real thought is required keep using the right scales and rulers to solve the problem at hand. A research scientist might use one set of rules in chasing a new result in the field, and another when designing an instrument used in gathering data for that result, and still another when coming up with a bit of office automation to handle the drudgery of the required paperwork.

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 18, 2021, 02:35:12 am
content: Quote from: DrG on February 18, 2021, 01:35:44 amI do believe the top to bottom approach is superior to the merged approach, then. (Yes, I meant hitting 1 if it is realistic difference from 0, given the threshold difference.)Quote from: CatalinaWOW on February 18, 2021, 01:18:58 amWell, I was thinking more along the lines of how to generate the tables when you have say a pot or some such connected to your MCU, with a limited useful sample range (say, after filtering) – or some other source. If after simple filtering the usable range is say 13 to 142, inclusive, it is much cheaper (total resource use wise) to just use an array for the values than implement the arithmetic to scale it to a "standardized" range like 0 to 100.In other words, I was just trying to show another useful tool for solving the underlying problem.Quote from: DrG on February 18, 2021, 01:48:12 amNo– it is actually much, much older, with roots inart.Things likecolor theory(some colors being "warm", others "cold", and so on; later, how colors affect depth perception), and perspective are at the roots of psychovisual modeling. Even things like cartoon stylisisms (cel shading) have its basis in there. Same for hearing/audio, especially scales, harmonies etc.We discovered the rules first, and only afterwards why those particular rules.For example, did you know that you can derive two-point perspective directly from optics using basic linear algebra? I've done it myself; it's a funky feeling, especially because I genuinely believed two-point perspective is just one of those very close approximations that hold in many ordinary situations but not always...The first diagram of two-point perspective is inDe Artificiali Perspectiva(1505) by Jean Pélérin (aka Viator;at archive.org). Descriptive geometry came first; it just also happens to be mathematically exact.Essentially, after we developed mathematics to examine the rules discovered, many of them proved to have a rather simple physical basis (perspective is trivial in basic 3D algebra, most human senses use an exponential scale, etc.); and some, like the average sensitivity of human hearing as a function of frequency (used heavily in audio compression, to distribute compression artefacts so that theirperceptionis minimized) were found to have their basis in the combined operation of the sensory organ itself and how it is processed in the human brain.So, the termpsychophysicsis not derived from psychology, but really from physics: describing in a scientific manner how the human sensorium works. Humanthoughtsor the inner self or things like that are NOT involved! For that reason, for visuals, I prefer the term "psychovisual modeling", although it means the exact same thing. Or, perhaps,physiophysics...

author: DrG
author_rank: Super Contributor
timestamp: February 18, 2021, 03:24:01 am
content: Quote from: Nominal Animal on February 18, 2021, 02:35:12 amI so liked your post until that last line.No. Neuroscience (or Psychology, as far as I am concerned) is the study of behavior. The purpose of the Nervous System is to produce behavior. If you want to study the nervous system, it only makes sense to study behavior.The harsher side of the engineer and the physicist looks at vision as a matter of the sensory activity of a few, relatively simple, transducers. They will talk endlessly about theories of color as though the rest of the CNS was not involved - it is funny. Those transducers like rods and cones are the LEAST important aspects of the perception of color and light intensity.I (well not me personally) can stick you in total darkness, stick a couple of electrodes in your brain and get you to experience all kinds of colors, just like you have been experiencing them the day before. No light at any frequency need. Period. Full Stop."Humanthoughtsor the inner self or things like that are NOT involved!" No my friend, they are the ONLY things that are involved.But, hey, it has been a fun and productive thread, I appreciate the diversity of view points and I sincerely thank you for your participation - I learned stuff!

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 18, 2021, 04:10:05 am
content: Quote from: DrG on February 18, 2021, 03:24:01 amBah, me fail English anyway. I just meant that psychophysics has everything to do with perception and brain function and biology, and nothing to do with social psychology or psychoanalysis (that I believe most people associate most with the "psycho-" prefix).

author: Vindhyachal.takniki
author_rank: Frequent Contributor
timestamp: February 20, 2021, 03:35:57 pm
content: i tried values as in last post, led intensity seems bit linear, but not very good.

author: DrG
author_rank: Super Contributor
timestamp: February 22, 2021, 04:58:37 pm
content: Quote from: Vindhyachal.takniki on February 20, 2021, 03:35:57 pmTry some other values

author: Renate
author_rank: Super Contributor
timestamp: February 22, 2021, 05:11:31 pm
content: A 0-100 output (as in percentage) is not particularly useful as counters/timers don't particularly care about being decimal.You could make the input only 0-100 but that only gets you less resolution.I doubt that any users sit there deciding, "Do I feel like 66% or 67% backlight today?"Use an 8 bit to 8 bit mapping.16 brightness levels is easy to do: table[17*i]Code:Steps 2 4 6 16 18 52 86 256Multiplier 255 85 51 17 15 5 3 1On the other hand if you still want 0-100 input just do table[(163*i+32)>>6]So why a 256 table if we only use a fraction of it?So that when the user powers the unit up it may fade in gracefully.So that we may smoothly ramp from one brightness to another when a user suddenly decides that a brightness of 6 is not adequate and they want 7.So that when the user shuts the unit down it may fade out gracefully.Code:0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02,0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,0x05, 0x05, 0x06, 0x06, 0x06, 0x08, 0x08, 0x09, 0x09, 0x09, 0x0a, 0x0a, 0x0a, 0x0a, 0x0b, 0x0b,0x0b, 0x0c, 0x0c, 0x0c, 0x0d, 0x0d, 0x0d, 0x0e, 0x0e, 0x0f, 0x0f, 0x0f, 0x10, 0x10, 0x11, 0x11,0x11, 0x12, 0x12, 0x13, 0x13, 0x14, 0x14, 0x15, 0x15, 0x16, 0x16, 0x17, 0x17, 0x18, 0x18, 0x19,0x19, 0x1a, 0x1a, 0x1b, 0x1c, 0x1c, 0x1d, 0x1d, 0x1e, 0x1f, 0x1f, 0x20, 0x20, 0x21, 0x22, 0x22,0x23, 0x24, 0x25, 0x25, 0x26, 0x27, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d,0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4f,0x50, 0x51, 0x52, 0x53, 0x55, 0x56, 0x57, 0x58, 0x5a, 0x5b, 0x5c, 0x5e, 0x5f, 0x60, 0x62, 0x63,0x64, 0x66, 0x67, 0x69, 0x6a, 0x6c, 0x6d, 0x6e, 0x70, 0x71, 0x73, 0x74, 0x76, 0x78, 0x79, 0x7b,0x7c, 0x7e, 0x80, 0x81, 0x83, 0x84, 0x86, 0x88, 0x8a, 0x8b, 0x8d, 0x8f, 0x91, 0x92, 0x94, 0x96,0x98, 0x9a, 0x9b, 0x9d, 0x9f, 0xa1, 0xa3, 0xa5, 0xa7, 0xa9, 0xab, 0xad, 0xaf, 0xb1, 0xb3, 0xb5,0xb7, 0xb9, 0xbb, 0xbd, 0xbf, 0xc1, 0xc4, 0xc6, 0xc8, 0xca, 0xcc, 0xcf, 0xd1, 0xd3, 0xd6, 0xd8,0xda, 0xdc, 0xdf, 0xe1, 0xe4, 0xe6, 0xe8, 0xeb, 0xed, 0xf0, 0xf2, 0xf5, 0xf7, 0xfa, 0xfc, 0xff