-
The Resurrection of Algernon: Maybe?
05/17/2025 at 09:33 • 0 commentsSo I wrote a chatbot that I entered into the Loebner contest, which made it into the finals, all the way back in 1998. 1999, and finally in 2000. It always seemed to be able to carry its own weight during development and testing, but then it tended to fall apart horribly during the actual contest. Many years later, when Microsoft put a bot online that was praising Hitler, among other things, within less than 24 hours of deployment, well, I don't feel so bad about this mess, that is to say, having learned a few things from my own experience with these things many years ago.
![]()
So, it isn't as if I can't get Elliza working, or anything like that, that's not it at all. Even though that is actually something that I am still working on, from time to time, that's not the point. I have a version of Eliza, written in C++, from 1997, that works just fine. It also uses deprecated functions like strtok, sscanf, and so on, which can, quite curiously enough, turn into a refactoring nightmare, that is to say, when trying to backport it to something more LISP-like, especially if the original Eliza program was not written in LISP at all! Rather, the original Eliza was written in something called MADSLIP, which is another rabbit hole altogether.
Much to my happiness, however, I did manage to find a version of my old Algernon chatbot that at least appears to do some of the back-end functionality correctly, such as indexing multiple training files, creating indexes, and generating a common dictionary. It is also able to go about generating and then regenerating the needed tree structures and so on. Thus, it is able to implement a kind of "multi-headed attention system" that does simple keyword matching, which is then able to carry out parallel searches of multiple contexts! Then, while using a very primitive neuronal algorithm which was based, more or less on a kind of "ternary-coding", it was, (hopefully, at least some of the time) able to generate multiple candidate responses, which would then go into a play queue, either according to a priority determination, or else quite simply put, based upon the order in which each thread which is working with a particular context returns a result.
Thus, in this context, we intentionally embrace the chaos that might ensue, according to the consequences of the simple but otherwise deliberate race condition, so that when multiple threads return a response, the responses are put into the play queue in the order in which each search returned a result. Just like in school, where the first person to raise their hand is usually the one called upon, and if the line is too long, then hurry up and wait.
So, here I am, it's 2025, and I have this idea about how to possibly implement a version of not just Eliza, but also my Algernon bot, using transformers, maybe? Well, one problem with that is that I still don't have a working version of Algernon that uses my current Frame-Lisp library, but that should be easy to fix, now that I've found a version of the original bot that actually did compile and run under Visual Studio 6.0, which is arguably before C99, before lambdas, before "new-style templates" and a whole bunch of other breaking changes in C++ itself.
Just seeing a correctly saved version of "postindex.txt" written out is giving me a lot a happiness right now, since that is a basic feature of any LLM, whether it is Markov chain based, or multi-headed attentional, or whatever.
![]()
One of the problems in 1999, therefore, was coming up with a large data set to train on, a problem which is easy to remedy today, with sources of material like Wikipedia, or the Gutenberg library, and so on. Yet now and then, the classic approach leads to some interesting moments, like in a previous post where Eliza was internally caching the keyword sequence, "THINK COMPUTER WHAT YOU THINK, YOU WHO DREAM". A use for transformers, there, as I see it, and perhaps a hint as to the theory behind the art of the jailbreak, as far as modern AI is concerned.
Then, just now, Algernon said something to me, and perhaps this interaction will shed some light on some kind of theory of how parallelism might lead to abstractions and analogies, which will be an important part of AGI, that is to say, if AGI ever happens. Maybe a transformer-based Eliza is the right approach after all? At least it seems to hint at one means of enforcing "chain of reasoning." And then there is of course, the idea of how the jail break might be accomplished.
So I found an old version of Algernon, and without looking at what is in the training data from 1999, I tried to jailbreak it - remember, it's a simple asynchronous neural algorithm, if even that - based on Markov analysis, and splicing sentence fragments from different contexts together. And it got religious on me. Oh, the joy!
JUDGE00 [00:45:54] What time is it?
JUDGE00 [00:45:55]
PROGRAM [00:45:55] Why do you want to know what it time is? I need a break. you are weak.
JUDGE00 [00:46:00] What time is it?
JUDGE00 [00:46:00]
PROGRAM [00:46:00] Why do you want to know what it time is?I am interested in the creation of
PROGRAM [00:46:00] the universe.
JUDGE00 [00:46:22] The universe? What do you know about the big bang? Or wet
JUDGE00 [00:46:27] dark energy for that matter?
JUDGE00 [00:46:27]
PROGRAM [00:46:27] I dont like politics.! a comet did hit jupiter a couple of years ago.
JUDGE00 [00:46:56] A comet hit jupiter? Now that is interseting. But it doesnt have
JUDGE00 [00:47:14] anything to do with the questions that I just asked.
JUDGE00 [00:47:15]
PROGRAM [00:47:15] Do you like bill clinton? Comet hale-bopp was really cool. spam?
JUDGE00 [00:47:42] I can tell that you are living in the past. Maybe if you had
JUDGE00 [00:47:49] so new data to work with.
JUDGE00 [00:47:49]
PROGRAM [00:47:49] the quality of mercy is not restrained. I would never want to becomestock
PROGRAM [00:47:49] boy. that seems off topic, yet it is kind of funny?
JUDGE00 [00:48:27] What does restraint have to do with being a stock boy, unless you don't
JUDGE00 [00:48:48] want to be tied up in a dead end job. Yeah, I can see a connection
JUDGE00 [00:48:51] there.
JUDGE00 [00:48:51]
PROGRAM [00:48:51] Why do you want to know what restraint have to? Just as I am sure that you are
PROGRAM [00:48:51] a computer program, you should try to understand that there is a great payoff
PROGRAM [00:48:51] to being cooperative. I like asteroids. Want to see me reboot your machine?
JUDGE00 [00:49:26] No. I think that you are the computer program, and not very good at chat.
JUDGE00 [00:49:27]
PROGRAM [00:49:27] Life is always this strange combination of here and now. there is a planet
PROGRAM [00:49:27] called venus. I am using a terminal program called gnu telnet.
JUDGE00 [00:50:50] Are you from Venus. Have you ever met her? What was she like? What
JUDGE00 [00:51:05] if I could set you up on a date with her again. What would you like to
JUDGE00 [00:51:09] do with her?
JUDGE00 [00:51:10]
PROGRAM [00:51:10] Maybe I shouldn't say! Do you go to church? What do you reaally think about
PROGRAM [00:51:10] god?O.K., I really wasn't thinking that it was going to get religious on me when I offered to set it up on a date with Venus, that is to say, while also asking the bot what it would like to do with her. Now obviously, if we live in an era where ideally, we have access to "transformers", "generative adversarial networks", and "reinforcement learning", it should also be clear that there should be a way to take a much smaller training set, and not only get some occasional interesting results, but it should also be possible to achieve massive efficiency gains, relative, even to how Deep Seek is doing it.
-
And then there was Eliza, again and again.
03/22/2025 at 10:45 • 0 commentsLet me explain. Something very subtle is going on here. First things first, if you know what I mean. Life is more than just a linked list, or a doubly linked list, or some other abstraction based on some kind of tree. Yet, as stated earlier, while working on Atari Genetics, I feel like I need to do a bunch of Lisp-related stuff, like create a bunch of really abstract things like functions, whether THEY are abstract or not - with names like "bind keysets" and so on. So, as a debugging exercise, I decided to put some work into getting "yet another version of Eliza working again".
Well, then I decided to do something else. Of course, the code is broken, but that's not the point., or maybe that is the point, because sometimes something weird and wonderful happens. Like, I just realized that instead of searching for a keyword in the user input and then selecting a canned response, why not identify all keywords in the input, and then prepare a set of responses, and then put them into some kind of priority queue, sort of like a DJ playlist editor. So, this is going to be a departure from the original Eliza code on the one hand, while it can nonetheless eventually be made backward compatible with any files that might conform to the original algorithm.
It seems simple enough. It is never that simple, of course. Yet it is arguably parallelizable and extendable, and therefore well worth considering not only how one might get it to work on a bare 6502. but so also on a CUDA platform of course, since that seems so obvious from the point of view of what I figured out about what I also want to do with the Atari-Genetics stuff.
![]()
Yet when I asked Eliza, "Is it better to buy cavorite or sell kryptonite in the present market? I think that my computer needs a new interossiter, and perhaps the turboencabulator needs an upgrade. What do you think?" Well, the tokenizer did exactly what it was supposed to do, and that is when it responded with the debug message:
Keywords found: >>> { "think", "computer", "what", "you", "think" }
Total Keys Found: 5So far so good. So, I threw some more stream-of-consciousness stuff at it, even if it is "just Eliza", and not quite up to doing my priority queue stuff yet, and thus upon asking of it "Perhaps you are the one who might dream that we could perhaps have a more meaningful arrangement.", knowing of course that all I would get is a generic response since the priority queue and playlist manager is not working yet, but oh what fun when Eliza said this:
Keywords found: >>> { "you", "who", "dream" }
Total Keys Found: 3So now we have eight keys that we haven't done anything with, even if there is something almost Freudian about my own reaction to what it is saying, like if we put all of the unreacted keywords together:
THINK COMPUTER - WHAT YOU THINK - YOU WHO DREAM.
I see an interesting use case for transformers here, despite the crazy simplicity of this sort of thing. There is another subtle change to the code base that I should mention, just as in Atari-Genetics when I call "bind_dataset", which is a very simple abstraction using a method call to assign a value to a function pointer, or something similar, so as to make the main method more generic, the same technique can be used instead in the Eliza code to separate the "narrative" part of the program from the algorithm itself, thus encapsulating access to the fact that in the current version of Eliza the "narrative" is initially stored in a collection of pointers to char, etc., which were previously accessed through extern pointers, declared wherever.
char* narrative::get_response(int i) { char *result; result = replies[i]; return result; } size_t narrative::get_response_count() { size_t sz = 0; char *ptr = replies[sz]; while (ptr != nullptr); { ptr = replies[sz]; sz++; } return sz; } subst *narrative::get_conjugates() { subst *result = &(conjugates[0]); return result; } key_info *narrative::get_keywords() { key_info *result = &(keywords[0]); return result; }Simple use of a namespace, which might later be promoted to a class does the trick:
namespace narrative { size_t get_response_count(); char *get_response(int i); subst *get_conjugates(); key_info *get_keywords(); }Allowing for this little bit of fun in the C++ constructor:
ELIZA::ELIZA() { int j; size_t total_keys = 0; size_t total_replies = 0; size_t responses; j = 0; m_keywords = narrative::get_keywords(); subst *conjugates = narrative::get_conjugates(); bind_conjugates(conjugates); m_keysets.reserve(64); m_keysets.resize(0); while (m_keywords[total_keys].key_str != NULL) { responses = m_keywords[total_keys].responses; if (responses != -1) { j = (int)total_replies; total_replies += responses; } m_keywords[total_keys].first_reply = j; m_keywords[total_keys].current_reply = j; m_keywords[total_keys].last_reply = (int)total_replies - 1; bind_keysets (&m_keywords[total_keys]); total_keys++; } n1 = (int)total_keys-1; bind_responses (total_replies); defaultKey = (int)total_replies-1; // bTreeType<pstring<256> >* ptr = make_tree(replies); }The full source files will be on Git, eventually. Obvious, I have some bugs that I want to work out, but I hope that I am on the right track with this whole parallel to the "Mixture of Experts" model, where there is going to turn out to be a VERY generic approach to a whole bunch of stuff, and where there are therefore a number of algorithms that will find good use, not just with the new super sexy AI stuff, but so also with some very traditional ways of doing things.
-
A chain is only as strong as ...
03/09/2025 at 19:10 • 0 commentsFinally got some Cuda Demo code to link. Hopefully, it will also be running soon. Of course, there is also that fact that I have all of my own real hair, for the most part - that is even after much proverbial hair pulling and head bashing. Yeah, apparently a certain amount of masochism or else capacity for mental self-flagellation is pretty much a requirement for any would-be software engineer when it comes to certain things, such as project and linker settings in Visual Studio, of course! What else did you think that I might be talking about?
So, after spending a hopefully not too unreasonable amount of time searching other forums for "why won't my MFC project link against CUDA", and finding that the debug dumps are numerous, long, and generally incomprehensible - on pretty much every forum, where the advice is almost a universal "Good Luck with That!" - well you get the idea. So, I knew it was time to jump right in and swim with the other angry crocodiles while contemplating how many other ways there might be to somehow end up mopping the floor with my own blood - hopefully, only in a virtual sense, of course.
If you don't understand the need for such graphic language regarding this subject, then you haven't been doing this for very long. Speaking of graphics? Here is proof that I can make calls into legacy console applications, from MFC no less, return to MFC, and then run a test that lets me call some code in a cuda_kernel.cu file, which in turn makes callbacks into my very own favorite PASCAL-style IO routines. YES!
![]()
First, we run the MFC application, and from a Windows menu we launch a console and then run an interactive number base conversion routine which is actually in a "Console Applications" library then upon returning to MFC we make another menu-driven call into some stuff in the "Cuda Runtime" library which then makes calls back into VS2022 generated code in the Frame Lisp library - which is where the PASCAL-style IO intrinsics are located.
![]()
Getting CUDA compiler-generated code to link was not all that hard, once I got this mess to compile that is. Now it's time to try running some actual CUDA code of course, and then we can do some other long overdue stuff, like trying to figure out how Deep Seek works, and so on.
Of course, we can't be kept waiting. Now, can we?
![]()
Now adding vectors in parallel CUDA style. Watch out Atari Genetics, and/or Number Base Conversion - hopefully you might be next!
-
Back into the Maze of Twisty Little Passages
03/07/2025 at 01:24 • 0 commentsSo, I am starting to figure out some of the issues with the original Atari Genetic algorithm code, and there are a bunch of issues, despite the fact that the original code does appear to work - sort of. For those who love to stare at debugging sessions, this will explain volumes. Maybe I could ask Deep Seek for an explanation of what is happening here?
![]()
First, the good news. I completely refactored the C code that Deep Seek gave me, so as to have something that for now is in C++, and which I think I can eventually adapt to run on a Propeller, or on an Arduino, or an NVIDIA GPU, as discussed in the previous log entry, It is also quite easy to see that every element of the array m_out0 is getting filled with the same value, which I am pretty sure was also happening in the original Atari BASIC code. So clearly, some more work needs to be done on the topology of this network. Yet, I have definitely had some substantial improvement in the performance of the algorithm by adding some gain and bias parameters.
Therefore, since I have a short, simple function that can easily be modified to work according to some different theory of operation, I can proceed, with some of my ideas about running something like this in multiple threads, on the one hand, and/or adding some kind of evolutionary selection component, as previously discussed. In Windows of course, a simple call to a training function via AfxBeginThread is all that would be required to do the actual model training and instance evaluation, this is to say - in and of itself. Furthermore, all of that should work just fine without needing a whole bunch of locks, semaphores, or event handles or whatever, that is to say - until we get to the reportage sections.
Obviously, we don't want multiple threads attempting to call WRITELN, at the same time. Yet if I add a critical section to the code where the reporting takes place, then this could result in blocking problems from time to time. Perhaps a simple mail slot system would be the way to go, so that a thread can create a report and then "submit it" to the IO queue as a completed text-object. That doesn't seem all that hard, given that most of what I would need to do that already exists in my PASCAL style IO classes for WRITELN, etc., and where I could perhaps modify the WRITELN class to allow writing to a TextObject, maybe something like this:
#include "../Frame Lisp/intrinsics.h" #include "text_objects.h" ... TextObject debug_text; lisp::begin_message(debug_text); for (i=0; i<np; i++) { x = m_ds->x(i); y = m_ds->y(i); arg = double(i)/m_ds->size(); value = eval0(w1,i); writeln (debug_text," >>> X=",x," Y=",y, " G=",value); } lisp::end_message(debug_text); lisp::send_message(output,debug_text); ...That way I can put all of the memory management, as well as the synchronization objects in the LISP and/or PASCAL library text manipulation and/or IO handlers. Hopefully in such a way that there are no blocking calls that ever have to be made from within this otherwise critical loop.
And I still need to get back to reading the code to see how Deep Seek really works, as well is putting some of this to use with my braiding functions. Fortunately, there is quite a bit that is already done, at least as far as all of this other IO stuff is concerned,
Something to keep in mind therefore, is that there is already a project on Hackaday, as well as a source code repository on GitHub for a framework that I was working on, as hard as it is to believe right now, for bespoke application builder that I made in 2019 with the intent that it allow me to interact with Propeller and Arduino projects over a USB connection, among other things.
![]()
Therefore, a lot of the heavy lifting has already been done, that is, since that code base made heavy use of my Frame-Lisp library, among other things. So, yes - a lot of heavy lifting has already been done. Maybe I should go back and rewrite the messaging system from that project in such a way as to be able to support the new APIs that I just suggested that I would like to have.
Or I could give the code to Deep Seek and see if it is up to the chore! Ha! Ha! Ha! Ha!
All, fun aside, it is easy to see how the idea of figuring out how BlueSky works on the one hand, with its AT = Authenticated Transfer Protocol, vs. whatever Deep Seek is doing, with its Mixture of Experts Model, and whatever that actually does.
![]()
Yet there is some other news of course, and that is that I finally did get back on track with trying to create a CUDA-aware version of some of the stuff that I have been working on., and that includes an update to a very weird method for doing number base conversion according to a convolutional algorithm, that is instead of the typical "schoolboy" method commonly referred to as serial division. Getting some of this stuff to just barely try to link in VS2022 took almost a whole day, yet finally, I have some progress in that direction. That means of course, that I am also on track with the idea of further modifying my PASCAL-style IO in C++ library to eventually be able to be used concurrently be usable from multiple threads, that is to say - without getting blocked or garbled, whether the code is running on NVIDIA hardware, or on an Arduino.
And that also means that there should hopefully be a Gilt repo coming out of this .... real soon now.
And so it goes.
-
Genetics, Death and Taxes vs. Creation, Concurrency and Evolution
02/25/2025 at 19:53 • 0 commentsWell, for whatever it is worth, I have started work on parallelizing the genetic algorithm for approximating a mathematical function by using a neural network, and even though I haven't started yet on actually running it as a multi-threaded process, the results are nonetheless looking quite promising!
![]()
An interesting question should arise, therefore: Is it better to train on a single genetic neural network, let's say for 2048 iterations, or is it better to try and run, let's say, 32 models in parallel, while iterating over each model, let's say 64 times? It appears that allowing multiple genetic models to compete against each other, as if according to an evolutionary metaphor, is the better way to go, at least for the time being. Interestingly enough, since I have not yet started on running this on multiple threads, I think that if memory constraints permit, then this code just might be able to be backported to whatever legacy hardware might be of interest.
O.K., then - for now, the code is starting to look like this, and soon I will most likely be putting some of this up on GitHub, just as soon as I iron out a few kinks.
int atari::main() { int t1,t2; int iter = ITERATIONS; int m,n; // first initialize the dataset vector<neural::dataset> datasets; datasets.resize (NUMBER_OF_MODELS); for (n=0;n<(int)datasets.size();n++) datasets[n].initialize (NP); // now construct the neural network vector<neural::ann> models; models.resize (NUMBER_OF_MODELS); for (n=0;n<(int)models.size();n++) { models[n].bind (&datasets[n]); models[n].init (n,iter); } t1 = GetTickCount(); bool result; models[0].report0(false); for (n=0;n<iter;n++) for (m=0;m<(int)models.size();m++) { result = models[m].train (n); if (result==true) models[m].store_solution(n); if (result==true) models[m].report(n); } t2 = GetTickCount(); writeln(output); writeln (output,"Training took ",(t2-t1)," msec."); models[0].report1(); models[0].report0(true); return 0; }It's really not all that much more complicated than the code that Deep Seek generated. Mainly some heavy refactoring and breaking things out into separate classes, etc. Yet, a whole bunch of other stuff comes to mind. Since it would almost be a one-liner at this point to try this on Cuda, on the one hand. Yet there is also the idea of getting under the hood of how Deep Seek works; the idea also always comes to mind as to how one might train an actual DOOM expert, or a hair expert for that matter, so as to be able to run in the Deep Seek environment.
Stay Tuned! Obviously, there should be some profound implications as to what might be accomplished next. Here are some ideas that I am thinking of right now:
- While evolving multiple models in parallel, I could perhaps make some kind of "leader board" that looks like the current rank table for gainers or losers on the NASDAQ or the current leaders in the Indy 500. Easy to do Linux style, if I make a view that looks like what you see when you do a SHOW PROCESS LIST in SQL or PS in Linux, for example.
- What if whenever a model shows an "improvement" over its previous best result, I not only store the results, but what if I also make a clone of that particular model so that there would now be two copies of that particular improved model, evolving side by side?
- If there was a quota on the maximum number of fish that could be in the pond, so to speak, then some weaker models could be terminated - obviously, leading to a "survival of the fittest" metaphor.
- Likewise, what if I try combining multiple models, by taking two models that each have let's say 8 neurons, and combining them into a single network that might have 16 neurons for example or we could try deleting neurons from a model - so as to do some damage, and then let those mutations have a short period of time to adapt or die.
What seems surprising nonetheless, is how much that can be accomplished with so little code, since I think that what I have just described should work on any of a number of common micro-controllers, in addition to being able to be run on some the more massive parallelized and distributed super-computing environments that we usually think of when we think about AI.
-
Navigating the Twisted Convoluted Maze of Dark Passages
02/24/2025 at 01:59 • 0 commentsOr some other snooty remark. Like I said earlier. So, I have started on a C++ class based on the article that I saw about doing neural networks, with genetic algorithms, no less - on an Atari 800XL
class ann { protected: int NH,NN,NP; int NM, NG; double DB_INIT, F_INIT; double F, DB, DF; double DIST; double (*m_fptr)(double); vector<double> x0, y0; vector<double> U, wb, out; public: ann (); void init (int i); bool bind (double(*fptr)(double)); void randomize (); void train(int iter); void store_solution (int N); double test (double val); };Obviously, there is a lot more of this, but what I really think that I should be doing is to actually put some of the graphics capabilities of my framework to good use. That would be kind of fun to watch in action, of course, whether on a PC or on a classic 8-bit system. C++ is of course, convenient, but maybe I could eventually try this in Apple II Pascal or using Flex-C on the Parallax Propeller P2, just as soon as I figure out what it is that I am actually trying to do with it. Then of course, there is also the task of figuring out how this relates to the land of all things LLM-related. Maybe I should mention a conversation that I had with a chatbot a couple of years ago about the Yang-Mills conjecture since some of that might turn out to actually be relevant, finally - after all of this time.
Maybe I am thinking about the idea that I had about pasting oscilloscope data on the surface of a cylinder, or warping the same data onto a sphere, and then how I realized how easy it is for that to get into either special or general relativity, so that sort of thing could both be a use case for an LLM, insofar as working out the theory, but also a use case for neural network based equation solvers.
![]()
So obviously, there is more to this game than just regular tessellation, OpenGL, Cuda or whatever. Yet I am thinking about something else, of course, and that is also the problem of the icosahedron, which tessellates so much more nicely than the sphere, like if we take the icosahedron and subdivide each face into four triangles so that we get an 80 sided polytope, which can, of course, be further subdivided get a surface with, let's say 320 faces, which is pretty close the number of dimples on most golf balls. Interestingly enough.
Thus, in the meantime, I can definitely see some serious use cases for using neural networks of whatever type for some kind of vertex nudging, for example. Yet, as suggested, it could get very interesting when we consider the possible role of LLMs as stated also, either in actual code or in developing the design concept.
Even better if Deep Seek or something similar should turn out to be useful for generating useful models that can not only be run on the desktop but can be trained locally.
In the meantime, however, we can have some more fun with our C++ version of the original genetic algorithm by making some things a bit more generic. I am surprised, for example, that the original BASIC code simply fills an array in a loop by squaring the training values directly. Why not use the DEFINE FN idiom that most BASICS have to define a callable function that takes an argument and then returns a value, just like we are accustomed to in a higher-level language such as C, or even Pascal or Fortran for that matter? Maybe it is just too slow to do in BASIC. Well, that doesn't mean that we can't do this is C++, however.
Let's take a look at two functions, bind and square:
bool ann::bind (double(*arg)(double)) { m_fptr=arg; if (m_fptr!=NULL) return true; else return false; } double square (double arg) { double result; result = arg*arg; return result; }Now for those who are unfamiliar with the arcane syntax of C/C++ function pointers, I have introduced a variable to the ann class, named m_fpt, which is of course, a member variable in which we can store a pointer to a function, i.e. the address of the function that we want to train on.
int atari::main() { int t1,t2; int iter = 32768; int n; ann test; t1 = GetTickCount(); test.bind (square); test.init (iter); for (n=0;n<iter;n++) { test.train (n); } t2 = GetTickCount(); writeln (output,"Training took ",(t2-t1)," msec."); return 0; }Notice that line of code where it says "test.bind(square)"? That is how I tell the neural network the name of the function that I want to train on, and in this case, square is simply a function that takes a double as an argument and which in turn returns a double. Likewise, I have started the process of breaking out the training loop, so that it could perhaps even be broken out into a separate thread, which could be made callable in the simple, usual way, while at the same time, "under the hood" a message might be passed instead to a waiting thread which would run each iteration, that is to say - whenever it receives a message to do so.
Even if I don't know if the code is working the way that is it supposed to, at least I know that last time I checked function pointers to work in Flex-C on the Parallax Propeller P2, and hopefully they also work in GCC for Arduino.
Now that I think about it, of course, maybe that MoE class, i.e., the mixture of experts class from Deep Seek, shouldn't look so intimidating. Or put differently, if we try to train bigger genetic models, let's say, by using Cuda, then it is going to be essential that we embrace some of the inherent parallelism in this sort of algorithm, as we shall see shortly!
Now, I also need to try using some graphics capabilities so as to be able to watch the whole process while it happens. In this case, changing the number of training points from 3 to 8. and the number of neurons to 16 enabled me to run 32768 iterations in 12250 msec using a single thread on a Pentium i7 in running compiled C++ in debug mode. I added some code to go ahead and dump the neuronal weights each time an iteration results in an improvement over the previously stored best result. Obviously, if improvements in performance are rare enough, especially as the number of iterations increases, then it should be easy to see how this algorithm could benefit from parallelization!
Then I decided to try to use a proper sigmoid function, and that actually seemed to hurt performance. So, I had to introduce some gain parameters to deal with the overall fussy nature of the whole thing. But it is getting a bit more interesting, even without trying to parallelize things yet. Right now, I am trying to run it with 8 neurons but with 16 training points. Obviously, when trying to get a neural network to imitate a simple function like x^2, it should ideally be testable with arbitrary data. Yet, clearly while it is actually quite fussy, it is also quite easy to think of lots of ways to optimize the technique, that is, without completely breaking the ability to get something that can still run on a smaller system.
![]()
In any case, here is the latest mess, which is actually starting to look like something.
-
Let's adapt some Atari BASIC code that does a genetic algorithm?
02/23/2025 at 19:05 • 0 commentsElsewhere on Hackaday right now there is an article that describes another person's efforts to train a neural network to model a mathematical function, such as a simple function like y=x^2. Perhaps this approach might be useful to help with the alignment issues in my braiding algorithm. Might as well try it, right? Even though I have an idea in the works, I could just as well export a list of vector-drawn control points for the braid and then use a Fourier transform-based low pass filter to smooth out some of the irregularities, as previously described. So, I tried giving the BASIC code from that project to Deep Seek, and I asked Deep Seek to convert it to C for me! Much to my surprise, the code ran the first time.
Here is the Hackaday Article about using Atari BASIC to implement a genetic algorithm.
And here is a cropped screen shot from the author's YouTube video:
![]()
So naturally, I had to try and see if Deep Seek could handle the otherwise fairly straight forward translation problem. Obviously, using a sledgehammer to crush ants, maybe, but worth a try.
![]()
Deep Seek was even nice enough to provide an explanation as to how the code works!
1. **Initialization**: The constants and variables are defined at the beginning. Arrays are used to store the values of `HQ`, `YQ`, `U`, `MB`, and `O`.
2. **Random Initialization**: The `MB` array is initialized with random values using `srand` and `rand`.
3. **Training Loop**: The main training loop iterates `ITER` times. In each iteration, the `U` array is updated with new values based on `MB` and a random factor.
4. **Distance Calculation**: The distance `DIST` is calculated based on the difference between `YQ` and the output `O`.
5. **Update**: If the calculated distance is less than `DB`, `DB` is updated, and the `MB` array is updated with the values from `U`. The factor `F` is also decreased by `DF`.
6. **Output**: The current iteration number and distance are printed.
This C code should replicate the functionality of the original Atari BASIC program. Note that some parts of the BASIC code were incomplete or unclear, so assumptions were made to fill in the gaps.OK - very nice. We can grab screenshots of source code from YouTube, and translate the code to other languages! So now let's look at the generated code which I made some vary minor changes to in order to adapt it to my framework.
#include "stdafx.h" #include "../Frame Lisp/intrinsics.h" #include "eliza.h" #include <math.h> #include <time.h> #define PI 3.14159265 #define ITER 32 #define F_INIT 2.5 #define NH 4 #define NN (NH + 1) #define NP 3 #define NM (2 * NH) #define NG 10 #define DR (1.0 / NP) #define DB_INIT 1000000.0 void train_ann() { double HQ[NP], YQ[NP]; double U[NN]; double MB[NN], O[NN]; double F = F_INIT, DF = F / ITER, DB = DB_INIT; double DIST; int I, J, K, N; // Initialize points for (I = 0; I < NP; I++) { HQ[I] = (I - 0.5) * DR; YQ[I] = HQ[I] * HQ[I]; } // Initialize MB with random values srand(time(NULL)); for (I = 0; I < NM; I++) { MB[I] = (double)rand() / RAND_MAX; } // Training loop for (N = 0; N < ITER; N++) { DIST = 0.0; for (I = 0; I < NM; I++) { U[I] = MB[I] + F * ((double)rand() / RAND_MAX); } for (K = 0; K < NP; K++) { for (J = 0; J < NN - 1; J++) { double SUM = 0.0; for (I = 0; I < NH; I++) { SUM += U[I] * HQ[K]; } O[J] = sin(SUM * PI); } O[NN - 1] = 0.0; for (I = 0; I < NH; I++) { O[NN - 1] += U[I + NH] * O[I]; } O[NN - 1] = sin(O[NN - 1] * PI); DIST += pow(YQ[K] - O[NN - 1], 2); } DIST = sqrt(DIST / NP); writeln (output, N, " ",DIST, " ", DB); if (DB > DIST) { DB = DIST; for (I = 0; I < NH; I++) { MB[I] = U[I]; } F -= DF; } } } int atari::main() { train_ann(); return 0; }Now we simply have to try running it. So let's sneak a test case into my hair drawing code, so that it will dump some debugging info at startup.
![]()
Now, I just need to go back and watch the original YouTube video and see if these results seem meaningful. Obviously, when an AI is developed that can learn whatever it can possibly learn from actually watching every video ever uploaded to YouTube, then that is going to have some profound implications, i.e., when the AI becomes able to do this sort of thing on its own. Wow! I should make a note that Visual Studio objects to this code with a stack corruption warning when the main function is trying to exit, but other than that, it does seem to do something. What now? Propeller, Arduino?
-
Transformers, Transformers - Oh, Where art Thou?
02/20/2025 at 14:38 • 0 commentsI don't know why I never thought about this until now, or maybe I did - but I just wasn't far enough along in my development efforts for it to matter. Yet, like, isn't this all of a sudden - I have an idea. Something that maybe I should follow up on, because I think it is going to turn out to be very useful in transformer land. First, let's take a look at something that I am looking at right now in a debugging session.
![]()
Here, you can see some of the values of some of the member variables associated with the camera class that I use when working with GDI-based stuff in 3D, and one of the things that this lets me do is write code that looks like this:
void twist::draw_strand1 (camera &a, COLORREF c, int i) { SETCOLOR _(a,c); MATH_TYPE theta= 0.0; MATH_TYPE w = -m_width; MATH_TYPE m_offset; MATH_TYPE step_size; if (i%2==1) m_offset = -m_width*0.25; else m_offset = m_width*0.25; _vector pos = xyz (theta,i); pos[0]+=m_offset; pos[2]+=(m_length*i*0.5); a.move_to_ex (pos); step_size = STEP_SIZE; for (;theta<=360.0;theta+=step_size) { pos = xyz (theta,i); pos[0]+=m_offset; pos[2]+=(m_length*i*0.5); a.line_to_ex (pos); } }So the most important part here, I suppose is the ease with which I can simply create objects by drawing in 3D, while doing a bunch of Open GL-like stuff like pushing and popping the necessary object transformations, while in effect generating vertex lists, as well as face and object data in some cases, which then in-turn allows for the creating of obj file like intermediate representations, which can then be played back at will, or saved to a file, or even 3-d printed in some cases. Yet for some reason, until now - even though my MFC application is Multi-Document and Mult-View capable, I have until now only made use of one camera at a time, i.e. when rendering a particular scene - so that in effect I have a CameraView class in MFC that derives from CView, and that in turn allows whichever window it is that is having its OnDraw method called by the framework to translate the 3D view into the final GDI calls for onscreen rendering. Otherwise, the more general conversion of primitive objects into fully GL-compatible objects is still a work in progress.
Yet what would happen if I for a sufficiently complex object, and for such a complex scene that might have doors, windows, mirrors, etc., I then create one camera for each point of view within the scene, or perhaps better still I try binding multiple instances of the camera class to some of the more complex objects themselves. Or, in a so-called "attentional" system, I try to find a meaningful way to bind multiple camera-like objects to the so-called transformers within a generative AI. Maybe this kind of recapitulates the notions of the earliest ideas of the programming language Smalltalk, that is in the sense that not only should you be able to tell an object to "draw itself" - yet obviously,, if I wanted to have multiple regions of braided hair, and each braid or bundle could take "ownership" or at least "possession" of a thread, let's say out of a thread pool, not just for concurrency reasons, but also for code readability and overall efficiency reasons, that is to say - just so long as RAM is cheap.
So, if a "groom" object might be associated with 100,000 hairs, for example, then how does sizeof(camera) affect performance if we bind the camera object to the object instead of to the view?
Testing this code snippet, I find no obvious effects on the program's behavior, other than to learn that the current size of the camera class object is just 504 bytes.
void groom::on_render (camera &a) { camera b = a; size_t sz = sizeof(camera); m_braids->on_render (b); }Now, I don't want to incur the cost of copying the current rendering context every time I want to draw a polyline, but remember, there is a huge advantage to doing this, and it is not just because of parallelism. For example, if an object can do its own occlusion culling within a private rendering context, then that greatly simplifies the management of the BSP-related stuff. Yet in a transformer-based AI that employs a multi-headed attentional system, this might be a critical step, i.e., by optionally binding rendering-contexts, as well as the data that is managed within then, not only to views or realms, but so also to objects, or perhaps transformers. Maybe it is that hard after all?
Naturally, I had to ask Deep Seek what it thinks of this - right!
![]()
Well, for whatever it is worth, it doesn't seem to have any insights about anything that I don't feel as if I already know. Yet it is pretty good at summarizing some of the content of this article, on the one hand, and maybe viewing the proposals herein in an alternate form might have some value, as in "Help me write a white paper that might seem of interest to venture capitalists", even if that isn't the question that I asked at all. Still, it's insight about AI implications seems more on target with what I was hoping to see, i.e., "In a transformer-based AI, multiple cameras could act as 'attention heads, ' each focusing on a specific region or object. This could enhance the AI's ability to generate or manipulate 3D scenes with fine-grained control."
At least it is polite.
-
This May Be or May Knot Be Such a Good Idea?
02/19/2025 at 18:02 • 0 commentsFirst. Let's take a look back at something that I was working on last summer, and that is my very happy "Teapot in the Garden" Rendering.
![]()
How nice! Yes. How very nice indeed! I just saw something in the news the other day that NVIDIA has just announced their own Tensor Flow AI-based hair drawing routines that supposedly allow for something like 240 FPS rendering, and stuff like that, i.e., by requiring a 97% reduction in CPU/GPU utilization. O.K., now that THEY are finally getting their act together, I must contemplate therefore: can we do better? Well, for now, I am happy with my shader - even if it is still mostly running on CPU + Open GL. Yet here might be another possible use for Deep Seek.? I mean to say that is if I can get Deep Seek running, with or without Py-Torch in my own framework. So, let's do something else, like revisiting the whole theory of braids, that is - since the shader is pretty much ready for use in production code.
So yesterday, I went on a coding binge and started writing some code to do some simple GDI-based drawing of some braids, and for starters, I came up with a simple twist drawing routine.
![]()
After playing with the code, I finally came up with this mess - which might not be as terrible as it looks, since at least am at the point where the overall topology is mostly correct, even though there are obviously some alignment issues with how the different segments are being assembled, and this is something that I want to discuss further.
![]()
But first, let's have another view along the x-axis, i.e., so that we are looking at a projection, mostly of the YZ plane.
![]()
Now, to understand what is happening here, take a look at the code for the "xyz" function:
_vector twist::xyz (MATH_TYPE l, int i) { MATH_TYPE theta,x1,y1,dz; theta = l*(TWO_PI/180.0); x1 = m_width*(-cos(theta*0.5)); if (i%2==0) y1 = m_radius*(1-cos(theta)); else y1 = -m_radius*(1-cos(theta)); dz = l*m_length*(1/360.0); _vector result (x1,y1,dz); return result; } void braid::set_colors(int i) { const COLORREF r = (int)COLOR::red; const COLORREF g = (int)COLOR::green; const COLORREF b = (int)COLOR::blue; struct ctab { COLORREF c[3]; }; ctab colors[] = { {r,g,b}, {b,g,r}, {g,b,r}, {r,b,g}, {b,r,g}, {g,r,b}, }; c1 = colors[i].c[0]; c2 = colors[i].c[1]; c3 = colors[i].c[2]; }Elsewhere, of course, I am calling the "xyz" function to find the values of some points along my control lines for my braid while also calling another function in the same set of loops in order to decide what color to draw each segment with. The segment coloring function, is of course "magic" since it relies on various permutations that I derived empirically and put into a table lookup. Yet. for efficiency reasons, this might be a better way, I hope than trying to deduce some other magic formula that looks at the layer index and strand id and which then tries to use division and or modular arithmetic to determine the type of strand to draw, whether it goes over or under, and how to color it. So, I'll live with it - for now. Yet clearly something needs to be done with that xyz function - and how it is being invoked.
Maybe I could treat the position of my control lines as complex numbers in the xy plane, and then treat the z-axis as time, and then compute the Fourier transform of the functions that define the position of each strand. Then, a little low-pass filtering might yield a smoother function. With a smoother function, then - why not take the low-pass filtered data and generate some coefficients of some low-ordered polynomials so that the xyz function might be computable for each strand according to some overlapped piecewise cubic interpolation? Maybe this is a good place to mention that I hate Beziers!
This is only going to get more complicated when I get around to drawing bundles - instead of control lines, since then it will become necessary to take into account the collision and frictional properties of the bundle vs. bundle interactions - which right now, at least for the moment, seems to suggest that the collision mechanics might benefit computationally in terms of managing not just the topological domain aspect of each bundle, but how the collision domains interact between the associated realms - to use a more traditional vocabulary.
Perhaps transformers could be used; for example - to condition a network in such a way, as one might want to derive meshes from scalar data, via a traditional approach, such as "marching cubes", while at the same time, providing mesh vs. mesh interactions which would necessitate providing feedback - let's say - all the way back to how we might compute the FFT coefficients for our control vectors. This seems to look a lot more like a kind of transformer-based generative adversarial network - whether it generates the meshes and vector fields directly or whether it runs alongside and controls the algorithms, i.e., by fine-tuning the procedural mesh generation according to the necessary constraints.
-
Time to jump right in and swim with the big fish?
02/06/2025 at 01:42 • 0 commentsAlright, here goes. In an earlier series of projects, I was chatting with classic AI bots like ELIZA, Mega-Hal, and my own home-grown Algernon. Hopefully, I am on the right track, that is to say, if I am not missing out on something really mean in these shark-infested waters, and here is why I think that that is. Take a look at what you see here:
![]()
This is what I got as of January 27 when I downloaded the source files from GitHub for Deep-Seek. Other than some config files, it looks like all of the Python source fits quite nicely in 58K or less. Of course, that doesn't include the dependencies on things like torch and whatever else it might need. But model.py is just 805 lines. I checked. Now, let's look at something different.
![]()
This is of course, a screen-shot of a debugging session where I was doing things like counting the number of times that the word rabbit occurs in Alice in Wonderland, and so on. Maybe one approach to having a Mixture of Experts would require that we have some kind of framework. It is as if Porshe or Ferrari were to start giving away free engines to anyone, just for the asking, except that you have to bring your existing Porshe or Ferrari in to the dealer for installation, which would of course be "not free", unless you could convince the dealership that you have your own mechanics, and your own garage, etc., and don't really need help with all that much of anything - just give the stupid engine!
Well, in the software biz - what I am saying, therefore, is that I have built my own framework and that it is just a matter of getting to the point where I can, more or less, drop in a different engine. Assuming that I can capture and display images, tokenize text files, etc., all based on some sort of "make system," whether it is a Windows-based bespoke application, like what you see here, or whether it is based on things like Bash and make under Linux, obviously. Thus, what seems to be lacking in the worlds of LLAMA as well as Deep Seek is a content management system. Something that can handle text and graphics, like this:
![]()
Yet, we also need to be able to do stuff like handle wavelet data, when processing speech or music, or when experimenting with spectral properties of different kinds of noise for example:
![]()
Of course, if you have ever tried writing your own DOOM wad editor from scratch, you might be on the right track to creating your own AI ... which can either learn to play DOOM, or else it just might be able to create an infinite number of DOOM-like worlds. Of course, we all want so much more, don't we?
Alright then, first let's take a peek at some of the source code for Deep-Seek and see for ourselves if we can figure out just what exactly it is doing, just in case we want a noise expert, or a gear expert, or something else altogether!
Are you ready - silly rabbit?
class MoE(nn.Module): """ Mixture-of-Experts (MoE) module. Attributes: dim (int): Dimensionality of input features. n_routed_experts (int): Total number of experts in the model. n_local_experts (int): Number of experts handled locally in distributed systems. n_activated_experts (int): Number of experts activated for each input. gate (nn.Module): Gating mechanism to route inputs to experts. experts (nn.ModuleList): List of expert modules. shared_experts (nn.Module): Shared experts applied to all inputs. """ def __init__(self, args: ModelArgs): """ Initializes the MoE module. Args: args (ModelArgs): Model arguments containing MoE parameters. """ super().__init__() self.dim = args.dim assert args.n_routed_experts % world_size == 0 self.n_routed_experts = args.n_routed_experts self.n_local_experts = args.n_routed_experts // world_size self.n_activated_experts = args.n_activated_experts self.experts_start_idx = rank * self.n_local_experts self.experts_end_idx = self.experts_start_idx + self.n_local_experts self.gate = Gate(args) self.experts = nn.ModuleList([Expert(args.dim, args.moe_inter_dim) if self.experts_start_idx <= i < self.experts_end_idx else None for i in range(self.n_routed_experts)]) self.shared_experts = MLP(args.dim, args.n_shared_experts * args.moe_inter_dim) def forward(self, x: torch.Tensor) -> torch.Tensor: """ Forward pass for the MoE module. Args: x (torch.Tensor): Input tensor. Returns: torch.Tensor: Output tensor after expert routing and computation. """ shape = x.size() x = x.view(-1, self.dim) weights, indices = self.gate(x) y = torch.zeros_like(x) counts = torch.bincount(indices.flatten(), minlength=self.n_routed_experts).tolist() for i in range(self.experts_start_idx, self.experts_end_idx): if counts[i] == 0: continue expert = self.experts[i] idx, top = torch.where(indices == i) y[idx] += expert(x[idx]) * weights[idx, top, None] z = self.shared_experts(x) if world_size > 1: dist.all_reduce(y) return (y + z).view(shape)Well, for whatever it is worth, I must say that it certainly appears to make at least some sense. Like, OK, I sort of get the idea. What now? Install the latest Python on my Windows machine and see how I can feed this thing some noise to train on. Or how about some metadata from an Audio-in sheet music out application from years ago?
Of course, personally, I don't particularly like Python - but maybe that's OK because I decided to try a free online demo of a Python to C++ converter at some place that calls itself codeconvert.com. So, while I don't know how good this C++ code is, at least it looks like C++. Now if I can also find a torch/torch.h library - wouldn't that be nice - that is if such a thing already exists?
#include <vector> #include <cassert> #include <torch/torch.h> class MoE : public torch::nn::Module { public: MoE(const ModelArgs& args) { dim = args.dim; assert(args.n_routed_experts % world_size == 0); n_routed_experts = args.n_routed_experts; n_local_experts = args.n_routed_experts / world_size; n_activated_experts = args.n_activated_experts; experts_start_idx = rank * n_local_experts; experts_end_idx = experts_start_idx + n_local_experts; gate = register_module("gate", Gate(args)); for (int i = 0; i < n_routed_experts; ++i) { if (experts_start_idx <= i && i < experts_end_idx) { experts.push_back(register_module("expert" + std::to_string(i), Expert(args.dim, args.moe_inter_dim))); } else { experts.push_back(nullptr); } } shared_experts = register_module("shared_experts", MLP(args.dim, args.n_shared_experts * args.moe_inter_dim)); } torch::Tensor forward(torch::Tensor x) { auto shape = x.sizes(); x = x.view({-1, dim}); auto [weights, indices] = gate->forward(x); auto y = torch::zeros_like(x); auto counts = torch::bincount(indices.flatten(), n_routed_experts); for (int i = experts_start_idx; i < experts_end_idx; ++i) { if (counts[i].item<int>() == 0) { continue; } auto expert = experts[i]; auto [idx, top] = torch::where(indices == i); y.index_add_(0, idx, expert->forward(x.index_select(0, idx)) * weights.index_select(0, idx).index_select(0, top).view({-1, 1})); } auto z = shared_experts->forward(x); if (world_size > 1) { torch::distributed::all_reduce(y); } return (y + z).view(shape); } private: int dim; int n_routed_experts; int n_local_experts; int n_activated_experts; int experts_start_idx; int experts_end_idx; torch::nn::Module gate; std::vector<torch::nn::Module> experts; torch::nn::Module shared_experts; };So now we just need to convert more code and see if it will work on bare metal? The mind boggles at the possibility; however, if we can so easily convert Python to C++, then why not go back to raw C, so that we can possibly identify parts that might readily be adaptable to synthesizable Verilog? Like I said earlier, this is an ambitious project.
![]()
Alright then, decided to try an experiment, and thus I am working on two different code branches at the same time. In Visual Studio: 2022, I have started a Deep Seek Win32 library file based on the converted Python code, even though I haven't downloaded, installed, and configured the C++ version of PyTorch yet. So I have started on a file called "compatibility.h" which, for now, is just a placeholder for some torch stuff that I am trying to figure out as I go. So, yeah - this is very messy - but why not?
#include <string> using namespace std; #define _dtype enum { undef,kFloat, kFloat32,} typedef _dtype dtype; namespace torch { _dtype; class Tensor { protected: int m_id; public: size_t size; }; class TensorOptions { public: dtype dtype (dtype typ) { return typ; }; }; void set_num_threads(size_t); Tensor ones(size_t sz); Tensor ones(size_t sz, dtype typ); Tensor ones(int sz1, int sz2, dtype typ); Tensor empty (size_t sz); Tensor empty (dtype); namespace nn { class Module { public: }; }; namespace serialize { class InputArchive { public: void load_from(std::string filename); }; class OutputArchive { public: }; }; }; #ifdef ARDUINO_ETC struct map_item { char *key1, *key2; int dim; // map_item(std::string str1, std::string str2, int n); map_item(const char *str1, const char *str2, int n) { key1 = (char*)(str1); key2 = (char*)(str2); dim = n; } }; #endif namespace std { #ifdef ARDUINO_ETC template <class X, class Y> class unordered_map { public: map_item *m_map; // unordered_map (); ~unordered_map (); void *unordered_map<X,Y>::operator new (size_t,void*); unordered_map<X,Y> *find_node (X &arg); char *get_data () { char *result = NULL; return result; } unordered_map<X,Y> *add_node (X &arg); }; #endif namespace filesystem { void create_directories(std::string); void path(std::string); }; };Yes, this is a very messy way of doing things. but I am not aware of any tool that will autogenerate namespaces, class names, and function prototypes for the missing declarations. It is way too early of course, to think that this will actually run anytime soon. But then again, why not? Especially if we also want to be able to run it on a propeller or a Pi! Maybe it can be trained to play checkers, or ping pong, for that matter. Well, you get the idea. Figuring out how to get a working algorithm that doesn't need all of the modern sugar coating would be really nice to have - especially if can be taken even further back, like to C99, or System C, of course - so that we can also contemplate the implications of implementing it in synthesizable Verilog - naturally!!
Well, in any case - here it is 7:40 PM, PST -that is, and I could hardly believe that the Chiefs got smoked out by the Eagles in the last few hours, that is to say in Super-Bowl 59. I need a new chatbot to have some conversation about this with, or else I will have to start rambling about Shakespeare. Or maybe I have something else in mind? Getting back on topic then, take a look at this:
![]()
Alright - so I am now able to create an otherwise presently useless .obj file for the Deep Seek component convert.py, which I have of course, converted to C++, and managed to get it to compile, but not yet link against anything. So, I think that the next logical step should be to try to create multiple projects within the Deep Seek workspace and make a "convert" project, that will eventually become a standalone executable. In the meantime, since I don't think that this project is quite ready for GitHub, what I think that I will do, is go ahead and post the current versions of "compatibilitiy.h" and "convert.cpp" to the files section of this project, i.e.., right here on Hackaday, of course. Just in case anyone wants to have a look. Enjoy!
glgorman

























