This was originally my plan with the new classification system, but it was
undone because I had hoped to punt on the somewhat controversial
issue. Unfortunately, I see no other way. Here I attempt to summarize the
reasons why, many of which are specific to the design decisions of TAME.
Keep in mind that TAME is a domain-specific language (DSL) for writing
insurance rating systems. It should act intuitively for our use case, while
still being mathematically sound.
If you still aren't convinced, please see the link at the bottom.
Target Language Semantics (ECMAScript)
--------------------------------------
First: let's establish what happens today. TAME compiles into ECMAScript,
which uses IEEE 754-2008 floating-point arithmetic. Here we have:
x/0 = Infinity, x > 0;
x/0 = -Infinity, x < 0;
0/0 = NaN, x = 0.
This is immediately problematic: TAME's calculations must produce concrete
real numbers, always. NaN is not valid in its domain, and Infinity is of no
practical use in our computational model (TAME is build for insurance rating
systems, and one will never have infinite premium). Put plainly: the
behavior is undefined in TAME when any of these values are yielded by an
expression.
Furthermore, we have _three different possible situations_ depending on
whether the numerator is positive, negative, or zero. This makes it more
difficult to reason about the behavior of the system, for values we do not
want in the first place.
We then have these issues in ECMAScript:
Infinity * 0 = NaN.
-Infinity * 0 = NaN.
NaN * 0 = NaN.
These are of particular concern because of how predicates work in TAME,
which will be discussed further below. But it is also problematic because
of how it propagates: once you have NaN, you'll always have NaN, unless you
break out of the situation with some control structure that avoids using it
in an expression at all.
Let's now consider predicates:
NaN > 0 = false.
NaN < 0 = false.
NaN === 0 = false.
NaN === NaN = false.
These will be discussed in terms of classification predicates (matches).
We also have issues of serialization:
JSON.stringify(Infinity) = "null".
JSON.stringify(NaN) = "null".
These means that these values are difficult to transfer between systems,
even if we wanted them.
TAME's Predicates
-----------------
TAME has a classification system based on first-order logic, where ⊥ is
represented by 0 and ⊤ is represented by 1. These classifications are used
as predicates to calculations via the @class attribute of a rate block. For
example:
<rate-each class="property" generates="propValue" index="k">
<c:quotient>
<c:value-of name="buildingTiv" index="k" />
<c:value-of name="tivPropDivisor" index="k" />
</c:quotient>
</rate>
As can be observed via the Summary Page, this calculation compiles into the
following mathematical expression:
∑ₖ(pₖ(tₖ/dₖ)),
that is—the quotient is then multiplied by the value of the `property`
classification, which is a 0 or 1 respectively for that index.
Let's say that tivPropDivisor were defined in this way:
<rate-each class="property" generates="tivPropDivisor" index="k">
<!--- ... logic here ... -->
</rate>
It does not matter what the logic here is. Observe that the predicate here
is `property` as well, which means that, if this risk is not a property
risk, then `tivPropDivisor` will be `0`.
Looking back at `propValue`, let's say that we do have a property risk, and
that `buildingTiv` is `[100_000, 200_000]` and `tivPropDivisor` is 1000. We
then have:
1(100,000 / 1000) + 1(200,000 / 1000)) = 300.
Consider instead what happens if `property` is 0. Since we have no property
locations, we have `[0, 0]` as `buildingTiv` and `tivPropDivisor` is 0.
0(0/0) + 0(0/0)) = 0(NaN + NaN) = NaN.
This is clearly not what was intended. The predicate is expected to be
_strongly_ zero, as if using an Iverson bracket:
((0/0)[0] + (0/0)[0]) = 0.
Of course, one option is to redefine TAME such that we use Iverson's
convention in place of summation, however this is neither necessary nor
desirable given that
(a) NaN is not valid within the domain of any TAME expression, and
(b) Summation is elegantly generalized and efficiently computed using
vector arithmetic and SIMD functions.
That is: there's no use in messing with TAME's computational model for a
valid that should be impossible to represent.
Short-Circuiting Computation
----------------------------
There's another way to look at it, though: that we intended to skip the
computation entirely, and so it doesn't matter what the quotient is. If the
compiler were smart enough (and maybe one day it will be), it would know
that the predicate of `tivPropDivisor` and `propValue` are the same and so
there is no circumstance under which we would compute `propValue` and have
`tivPropDivisor` be 0.
The problem is: that short-circuiting is employed as an _optimization_, and
is an implementation detail. Mathematically, the expression is unchanged,
and is still invalid within TAME's domain. It is unrepresentable, and so
this is not an out.
But let's pretend that it was defined that way, which would yield this:
{ ∑ₖ(pₖ(tₖ/dₖ)), ∀x∈p(x = 1);
propValue = <
{ 0, otherwise.
This is the optimization that is employed, but it's still not mathematically
correct! What happens if p₀ = 1, but p₁ = 0? Then we have:
1(100,000/1000) + 0(0/0) = 100 + NaN = NaN,
but the _intent_ was clearly to have 100 + 0 = 100, and so we return to the
original problem once again.
Classification Predicates and Intent
------------------------------------
Classifications are used as predicates for equations, but classifications
_themselves_ have predicates in the form of _matches_. Consider, for
example, a classification that may be used in an assertion to prevent
negative premium from being generated:
<t:assert failure="premBuilding must not be negative for any index">
<t:match-gte value="premBuilding" value="#0" />
</t:assert>
Simple enough—the system will fail if the premium for a given building is
below $0.
But what happens if premBuilding is calculated as so?
<rate-each class="property" yields="premBuildingTotal"
generates="premBuilding" index="k">
<c:product>
<c:value-of name="propValue" index="k" />
<c:value-of name="propRate" index="k" />
</c:product>
</rate-each>
Alas, if `property` is false for any index, then we know that `propValue` is
NaN, and NaN * x = NaN, and so `premBuilding` is NaN.
The above assertion will compile the match into the first-order sentence
∀x∈b(x > 0).
Unfortunately, NaN is not greater than, less than, equal to, or any other
sort of thing to 0, and so _this assertion will trigger_. This causes
practical problems with the `_premium_` template, which has an
`@allow-zero@` argument to permit zero premium.
Consider this real-world case that I found (variables renamed), to avoid a
strawman:
<t:premium class="loc" round="cent"
yields="locInitialTotal"
generates="locInitial" index="k"
allow-zero="true"
desc="...">
<c:value-of name="premAdditional" />
<c:quotient>
<c:value-of name="premLoc" index="k" />
<c:value-of name="premTotal" />
</c:quotient>
</t:premium>
This appears to be responsible for splitting up `premAdditional` relative to
the total premium contribution of each location. It explicitly states that
it wants to permit a zero value. The intent of this block is clear: a value
of 0 is explicitly permitted and _expected_.
But if `premTotal` is for whatever reason 0—whether it be due to a test
case or some unexpected input—then it'll yield a NaN and make the entire
expression NaN. Or if `premAdditional` or `premLoc` are tainted by a NaN,
the same result will occur. The assertion will trigger. And, indeed, this
is what I'm seeing with test cases against the new classification system.
What about Infinity? Is it intuitive that, should `propValue` in the
previous example be positive and `propRate` be 0, that we would, rather than
producing a very small value, produce an infinitely large one? Does that
match intuition? Remember, this system is a domain-specific language for
_our_ purposes—it is not intended to be used to model infinities.
For example, say we had this submission because the premium exceeds our
authority to write with some carrier:
<t:submit reason="Premium exceeds authority">
<t:match-gt name="premBuilding" value="#100k" />
</t:submit>
If we had
(100,000 / 0) = ∞,
then this submit reason would trigger. Surely that was not intended, since
we have `property` as a predicate and `propRate` with the same predicate,
implying that the answer we _actually_ want is 0! In that case, what we
_probably_ want to trigger is something like
<rate yields="premFinal">
<t:maxreduce>
<c:value-of name="premBuildingTotal" />
<c:value-of name="#500" />
</t:maxreduce>
</rate>,
in order to apply a minimum premium of $500. But if `premBuildingTotal` is
Infinity, then you won't get that—you'll get Infinity, which is of course
nonsense.
And nevermind -Infinity.
Why Wasn't This a Problem Before?
---------------------------------
So why bring this up now? Why have we survived a decade without this?
We haven't, really—these bugs have been hidden. But the old classification
system covered them up; predicates would implicitly treat missing values as
0 by enclosing them in `(x||0)` in the compiled code. Observe this
ECMAScript code:
NaN || 0 = 0.
Consequently, the old classification system absorbed bad values and treated
them implicitly as 0. But that was a bug, and had to be removed; it meant
that missing indexes in classifications would trigger predicates that were
not intended to be triggered, if they matched against 0, or matched against
a value less than some number larger than zero. (See
`core/test/core/class` for examples.)
The new classification system does not perform such defaulting. _But it
also does not expect to receive values outside of its valid domain._
Consequently, _NaN and Infinity lead to undefined behavior_, and the
current implementation causes the predicate to match (NaN < 0) and therefore
fail.
The reason for this is because that this implementation is intended to
convey precisely the computation necessary for the classification system, as
formally defined, so that it can be later optimized even further. Checking
for values outside the domain not only should not be necessary, but it would
prevent such future optimizations.
Furthermore, parameters used to compile into (param||0), to account for
missing values or empty strings. This changed somewhat recently with
|
||
---|---|---|
bin | ||
build-aux | ||
core | ||
design/tpl | ||
doc | ||
progtest | ||
rater | ||
src | ||
tamer | ||
test | ||
tools | ||
.gitignore | ||
.gitlab-ci.yml | ||
.gitmodules | ||
.rev-xmle | ||
.rev-xmlo | ||
COPYING | ||
COPYING.FDL | ||
HACKING | ||
Makefile.am | ||
README.md | ||
RELEASES.md | ||
VERSION.in | ||
bootstrap | ||
c1map.xsd | ||
configure.ac | ||
package-lock.json |
README.md
TAME
TAME is The Algebraic Metalanguage, a programming language and system of tools designed to aid in the development, understanding, and maintenance of systems performing numerous calculations on a complex graph of dependencies, conditions, and a large number of inputs.
This system was developed at Ryan Specialty Group (formerly LoVullo Associates) to handle the complexity of comparative insurance rating systems. It is a domain-specific language (DSL) that itself encourages, through the use of templates, the creation of sub-DSLs. TAME itself is at heart a calculator—processing only numerical input and output—driven by quantifiers as predicates. Calculations and quantifiers are written declaratively without concern for order of execution.
The system has powerful dependency resolution and data flow capabilities.
TAME consists of a macro processor (implementing a metalanguage), numerous compilers for various targets (JavaScript, HTML documentation and debugging environment, LaTeX, and others), linkers, and supporting tools. The input grammar is XML, and the majority of the project (including the macro processor, compilers, and linkers) is written in a combination of XSLT and Rust.
TAMER
Due to performance requirements, this project is currently being reimplemented in Rust. That project can be found in the tamer/ directory.
Documentation
Compiled documentation for the latest release is available via our GitLab mirror, which uses the same build pipeline as we do on our internal GitLab instance. Available formats are:
Getting Started
To get started, make sure Saxon version 9 or later is available and its path
set as SAXON_CP
; that the path to hoxsl is set via HOXSL
; and then run
the bootstrap
script:
$ export SAXON_CP=/path/to/saxon9he.jar
$ export HOXSL=/path/to/hoxsl/root
$ ./boostrap
Running Test Cases
To run the test cases, invoke make check
(or its alias, make test
).
Testing Core Features
In order to run tests located at core/test/core/**
, a supporting environment
is required. (e.g. mega rater). Inside a supporting rater, either check out a
submodule containing the core tests, or temporarily add them into the
submodule.
Build the core test suite summary page using:
$ make rater/core/test/core/suite.html
Visit the summary page in a web browser and click the Calculate Premium button. If all test cases pass, it will yield a value of $1.
Hacking
Information for TAME developers can be found in the file HACKING
.
License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.