Showing posts with label Math. Show all posts
Showing posts with label Math. Show all posts

2025-04-16

JSON Update

A followup to a recent rant.

First an admission. At least one thing I complained about was not a feature of JSON per se but of the library (nlohman::json AKA JSON for Modern C++) that we're using. In particular the behavior of serializing a floating point IEEE-754 special to null, and then throwing when trying to deserialize a null into a floating-point variable is library specific. And they stand by it. Grrrr!

Second, by defining a strong-typing wrapper I was able to (de)serialize those values from-and-to strings. I even provided multiple acceptable spellings on the deserialize path. Then by writing explicit to_json and from_json routines for my objects (rather than relying on the handy-dandy macros in the library) I was able to apply the strong-types only at the point of (de)serialization reducing what initially looked like a major intrusion into the code. Yeah.

It's not a complete win, however, because I have a std::variant<double,std::string> in the code-base. The usual advice for deserialize a variant with the library is to detect the json-type of the value1 and use that to know which member type to set. Only the number can give rise to a string value. So I had to explicitly (de)serialize the current-type, too. Bletch!

Long story short. We're going ahead with this and I may replace ny custom interchange format after all. Just because other users will stand a better chance of decoding the JSON.

But the lack of infinity and not-a-number is still a bug and still renders the format poorly suited for use in numeric computing.


1 JSON for Modern C++ uses a domain-model object as an intermediary, so this is relatively easy.

2025-03-28

Actually, if I had it to do over again I still wouldn't use JSON

When I started with my current employer they asked me to do a "story-board version" of a tool they were pitching to a customer by way of a warm up project. When the customer picked it up they kept me on as the lead designer and coder.1 This was the first time I'd ever been put in charge of the basic decision making for a shared effort or of something intended as "a product", so unsurprisingly, I made some mistakes. There are facts about the codebase that I am not proud of.2 In a couple of cases I cringe inside when I have to explain them to newcomers.

Interchange format

Plain text is the way

We run a significant amount of computation in separate OS processes.3 Honestly, to an old Unix hand like me that feels completely natural and it's not much harder than threads except for one thing: you can't just hand the worker a binary config object, but have to invoke some kind of interprocess communication. I choose to pass a serialied config object.

Now, a wise developer would have picked an existing serialization library. Obviously. But I didn't understand any of the existing options and saw only obstacles, so I rolled my own.4

After all, I had very simple needs, so I had it done in an afternoon. Tests included. Then time passed, requirements changed, and the format grew more elaborate. The code got longer and spawed more templates and more specializations. Writing the tests got harder. We began using it for a save-current-configuration format. I learned how to use the type_traits header badly. Then better. That heloped with the tests, but only a little. Still more requirements loomed and the code became a no-go area for everyone but myself and my best junior dev (now really a mid-career dev).

For several years I've been telling people that if I had it to do over again we'd be using JSON.

Little did I know.

Careful what you wish for

A couple of months ago we found out that a subset of the tool is being used as a component in a multi-step chain by a different division at the customer's organization. Last month we got funding to support that effort. This month they asked us to replace the custom interchange format with JSON.

We picked a library and went to town. Progress is being made, but it's not quite all sunshine and butterflies.

Why JSON is a really bad choice for this application

The thing to understand up-front is that this is an application in physical modeling. We compute an approximation to the behavior of the real world in a highly specialized domain and we do it fast and with a friendly front end. We don't need "just so" fidelity, but it does need to be reality based.

That means numbers. Almost always in floating point representations.

Nearly all modern platforms support IEEE-754 floating point numbers, which I have mentioned before. A feature of that standard is the ability to represent several special cases without demanding extra space. The available special cases go by three names: "infinity", "negative infinity", and "not-a-number" (AKA NaN). The infnities are generated when you do things like divide a finite value by zero or take the logarithm of zero (which you'd expect from real math) but also when you do things "close" to the real ones like dividing a big enough number by a small enough one. NaNs come out of weird operations like dividing zero by zero, or taking the arcsine of two.

Programmers have limited control over the users and what kind of input they generate so these kind of values pop up from time to time, and we have to decide how to deal with them. It's one of the annoyances of the job. Sometimes you want to do one thing with infinities something else with NaNs.

My custom serialization format handles those values gracefully, but JSON does not:5 it serializes all of them as null.

Not only is this ambiguous, but readers are allowed to simply fail when they encounter null where they were expecting a number. In particular, the library we'd started using throws an exception in this case. Really. This may or may not be reasonable in some domains but it is clearly an error in scientific computing.

Now what?

The thing is, it's not going to get fixed. You'd have to add new tokens to the grammar which would break many (probably billions) of deploy instances. When one of the selling points of the format has been stability. Total nonstarter.

I'd been planning on deprecating the old interchange format: stop generating it (maybe even remove the generation code) but keep the reading code around for a while. Becuase of the save files, naturally.

Now I'm not sure I want to switch. Maybe I just want to support JSON as an alternative. And support extra code for the foreseeable future. Sigh. Or I can live with not being able to pass well defined specials from component to component. Bigger sigh.


1 Actually, at first (and from time to time since then) I was the only person on the project. But still.

2 And some I am proud of. And a couple I'm ambivalent about because they may be necessary, but they are complex and hard for newbs to wrap their heads around. Well, they were hard to figure out in the first place, too. At least I wrote design documents for the features, so I have an answer when they ask "What were you thinking?"

3 A legacy library we rely on for one of the main computationally bound features of the application is single-threaded and not re-entrant; to let our customers take advantage of their beefy, many-cored, analytic workstations we have to get the OS to isolate instances.

4 Seems to be a habit, doesn't it? Though, in my defnense the logger has been a success. It has ridden out expanding needs with minimal maintenance and without growing out of hand.

5 Interestingly, JSON is a strict subset of JavaScript which does support the specials. To judge from the few things I've seen from the creator, it's likely they were sacrificed on the alter of simplicity. Perhaps a lamb too far, that.

2023-01-30

Can you use fixed-point?

I do scientific computing. Mostly in c++ which offers a host of places to have problems, but that isn't what I want to talk about today. Instead I want to talk about language-independent issues with math.

From a science or engineering point of view, the formulae that we look up and equations we write down and manipulate all assume we're working with real or complex values (or at least with integers). Notably all those fields are infinite and we represent a working sub-set of them with not-very-big sets of bits. And that's where the trouble sets in.

Now, many language provide types that offer a "floating-point" representation of some of the reals. Think about a binary version of scientific notation: $1.xyz \times 2^{abc}$. In modern times this stuff is actually well standardized with most hardware implementing IEEE754.

A not-at-all exhaustive list of the common problems for floating-point representations include

  • Easy to write down fractions like $\frac{1}{3}$ don't have exact representations in floating point because the format is finite.
  • Worse, even fractions like $\frac{1}{5}$ that have finite representations in decimal notation don't have one in binary notation and so are also inexactly represented.
  • The Commutative and Associative rules for basic operations like addition and multiplication are lost in some circumstances.
  • It takes special care to insure that you can accurately round-trip a in-memory value through a textual representation.
  • As a result of the above, it is very easy to write down an expression that has an equal sign in the middle on paper, but when you compute the two sides in code and compare them with == it returns false.
  • As a result of library differences in IO routines and some functions even if you get it right on one machine/compiler combination it can break if ported to a different machine/compiler combination even if they both implement the same standard!
As a result of these and other details floating-point math is notoriously hard to use correctly. The more so if you worry about unreasonable inputs (as you must).

We use floating-point math anyway because it supports values over a huge range of magnitude (for the number of bits used in the representation) and often has a fast, hardware-supported implementation. Still, sometimes, if you know the use domain well enough you can select a more limited range of necessary values and use fixed-point math to avoid some of the problems with floating-point.

Recently at work we dealt with "it's not comparing right" problems for angles on the sphere by coding azimuth and elevation in terms of a integer numbers of arc-minutes which provided more than sufficient precision for our needs, gets along nicely with the domain practice of describing angles in degrees, means that each value fits into a 16-bit field, and can be reliably round-tripped through a customer-specified text format.

Alas, many languages (most that are promoted for scientific computing) don't have bulit-in types or library support for fixed point so it isn't always practical: you have to ask how you will implement any special functions you need before you make that choice.

But it is worth asking right at the start.