I did a little more google-searching and found a couple newsgroup postings that were even more useful than the original link. The articles are embedded in the diagram of the two implementations given below.
User-controlled inputs are:
1) original floating point value
2) requested max error
3) max allowed value for numerator
4) max allowed value for denominator
1) floating point value of rational approximation
2) numerator of rational approximation
3) denominator of rational approximation
4) actual error
5) boolean flag telling whether error spec was met
6) # of terms / iterations to achieve approximation
The first (and recommended) implementation is based on a continued fraction repr
esentation of the original floating point value. In my testing, the slowest execution time was about ~5x the fastest.
I left several array indicators on the front panel that helps show the progression of the approximation through all its iterations. So you can check out successive approximations for pi or something.
The second implementation is based on iterating through a sequence of "Farey" fractions. It's fastest case can be several times faster than the continued fraction method, but there are other cases that slow it down by a factor of 1000+. (It seems to be worst when the floating point value is very close to an integer, a situation that may arise fairly often.) Because of its virtually unbounded execution time, I advise against using it. Consider its inclusion here to be solely for educational purposes.
As suggested earlier in the thread, I'll post the continued fraction version as a piece of example code.
After recently linking to this thread, I noticed that I could no longer download the example vi using Chrome. I decided to repost, and in the process I did a little bit of minor tweaking as well as making a wrapper that helps translate from the terminology of rational approximation with its talk of numerators and denominators over to the terminology of DAQmx with its talk of buffer sizes and # of waveform cycles.
In the process, I also wound up upcompiling the code to LV 2016. FYI.
More than 30 years ago, before the era of PCs, when 65kB was as much memory as you could address directly, I programmed a "sum of sines" algorithm for a behavior stimulus that covered a frequency range of 0.02 - 1 Hz. The technique we used was to generate a waveform of 16,384 points that represented the 7th, 13th, 23rd, ... (all relatively prime harmonics, either 8 or 10 of them) played back so that the fundamental ranged from 0.001 to 0.01Hz (some of these number might not be quite right -- it's been a long time). The point is you need to have a buffer large enough to have an integer number of periods inside it. If it is a single frequency, you need one period's worth of points.
And yet another update. The topic came up again, I pointed here, and in the process realized that the wrapper utility I posted recently didn't have any terminal connections. I added those and then added yet another layer to demo how to connect it to an actual AO sine wave generation task. The zip below contains both the vi's posted above (after I added wiring terminal connections to one) plus a vi that integrates them with an AO task.
A few years later and here's another small update.
- Now attempts to auto-discover the board's master timebase. Uses front panel control value only if auto-discovery fails.
- Changed from Continuous Sampling to 3 buffers worth of Finite Sampling so the demo can stop on its own
- Added a few front panel indicators and clarified some front panel labels
It's also worth noting that this whole exercise filled more of a void back in the "old days." At some point DAQmx made non-regenerating AO tasks *much* more robust and reliable in the years since I originally posted this method. So today if you need a glitch-free solution, you could probably go with a non-regenerating Continuous Sampling task where you compute and write blocks of data on the fly for as long as the AO needs to last.
Of course if that's a very long time, the attached utility does the dirty work to fit the right # of waveform cycles into a buffer and let you use regeneration, which is even easier.