ModelCompiler
In college, I build a graphical calculator in BASIC on the original Apple ][. Formulas that were entered as a string would be directed through the BASIC interpreter and evaluated. At some point, I wanted to port the calculator to Pascal, another forgotten language. Since Pascal is compiled, I had to find a different solution for evaluating the equations, preferably a compile-once, use-often method.
The strategy I chose was to convert the (infix) equations to Reverse Polish Notation (postfix), an old classic. The evaluator was written in assembly and used the processor stack for speed. Then when ParX needed a model compiler I revisited this design.
The parser and the basic data structure for the parse tree were sound, and could be extended to handle the more general case needed for ParX: multiple equations, intermediate variables, and conditional evaluation.
Then it was time for the pièce de résistance: analytical first-order derivatives. Most optimization and root-finding algorithms gain speed and reliability when derivatives are made available, and ParX is no exception. Mathematical analysis provides the rules for determining the derivatives for all operators and special functions. So all we have to do is build parallel parse trees for each derivative, and prune the results to eliminate superfluous operations. It sounds straightforward, but it is still a lot of code.
The ModelCompiler was originally developed as a stand-alone application that could feed several other design tools. It was written in (K&R) C, and optimized for the anemic computers of the day (1995), which means a lot of aggressive memory management and dangerous pointer arithmetic. An integrated version of this code is now open source software under the GPLv3 license, and can be found on GitHub as part of the ParXCL project, the original command line version of ParX.
Then in 2016 the ModelCompiler was ported from C to Objective-C, for the purpose of creating a single ParX application for submission to the Mac App Store. In the transition it gained a number of long desired features: full Unicode support, a wider range of variable names, special numbers and physical constants, and unit-strings for the variables and parameters. This version of the code is now also open source software under the GPLv3 license, and can be found as an SPM module on GitHub
In 2025, it was finally time to make the next leap forward and port the ModelCompiler to Swift. The C version is an intricate mess of pointers and memory management (and even some goto's), which translates poorly to Swift, so I decided on a complete rewrite, using the original code only as a reference.
After a lot of regression testing, I sincerely believe that the Swift version is on par with the C version.
The only downside is that a "naive" implementation of the ModelInterpreter class
is about a factor 2.5x slower than the C reference design.
It seems that this factor is the expected price one has to pay for memory safety.
However, since we can check all array indexes when building the interpreter Code Stack,
we do not have to do so at runtime.
By copying all Swift arrays to an unsafeMutableBuffer
at initialization,
the overhead is reduced to a mere 30%.
The final difference in speed can be attributed to two causes:
In the C version, the Code Stack is an array of a union-type where all cases reduce to an integer.
In the Swift version, the closest equivalent is an enum-type with associated integer values.
For now, the optimizer has trouble unpacking this enum-type efficiently without type checking.
The second problem is the lack of true pass-by-reference function parameters in Swift.
Instead, the inout-parameters implement a copy-in/copy-out behavior
that cannot be circumvented with a unsaveMutableBufferPointer
.
The copy-out with range checking will always be performed.
During a typical ParX optimization run, about half the processing time is spent in the model interpreter. So in practice, the performance hit is only in the 10% to 20% range, which is definitely acceptable.
Model Specification Language
ParX models are represented as a set of implicit equations that must be zero in the solution. There is no artificial separation between independent and dependent variables. In combination with the availability of internal (i.e. auxiliary) and temporary variables, this allows for a much wider scope of models without the need for internal iteration. A full set of mathematical operators and special functions is provided, as well as an abundance of special numbers and physical constants. Conditional evaluation allows for the description of regional models.
The models are described in a text file that specifies its interface variables, parameters and equations between them, in a flexible format. For the details of the specification language visit: Model Specification Language.
As an example: the model for a MOS transistor as proposed by Shichman and Hodges.