-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support constants #90
Comments
Having thought more about this: no, a constant should not just be an instance of a unit. A "unit" could be basically any type, so we'd need SFINAE, which would be expensive. This argues in favor of using a It's probably still good to keep this template as a monovalue type, and use a labeled unit. |
Here's a running checklist for what it would take to call this feature "done". (I'll check things off as they're implemented locally in my client, rather than on remote.)
|
Right now, `get_value<T>(m)` for a magnitude `m` either produces the requested value, or invokes a hard compiler error via `static_assert`. It would be nice if we had a way to ask whether this operation would succeed. For this purpose, we now provide `representable_in<T>(m)`. It turns out that we can't actually answer this question without effectively doing all of the work to compute the value. We won't want to do this twice, because that would uselessly slow down compile times. Therefore, we extract all of our logic into a common implementation function. The logic is basically equivalent to the pre-existing version of `get_value<T>(m)`, except that instead of each `static_cast`, we return a unique value of a new `enum` we created for this purpose. This means that each function can get its result efficiently. For `get_value`, we change the trigger for each `static_assert` to the corresponding enum value. For `representable_in`, we simply check whether that enum has the `OK` value. This PR helps pave the way for #90. When we make our `Constant` class implicitly convertible to `Quantity` types, it turns out that we can do better than the "overflow safety surface"... because we know the exact value! We can achieve perfect compile-time checking. This PR will give us a single function that we can call in our implementation for that. Includes docs and tests. --------- Co-authored-by: Geoffrey Viola <geoffviola@users.noreply.github.com>
We may want to add some documentation for how to add a new constant, after we settle on the presentation. |
Constants (aurora-opensource#90) and symbols (aurora-opensource#43) have a lot of similarities. For example: - Multiplying by a raw number produces a quantity. - Multiplying by a quantity changes the units of the quantity. - They can compose with other instances of the same family (e.g., the product of two constants is a constant). Let's use the term "wrapper" to refer to constants and symbols here. To make each wrapper as easy as possible to implement, I've created some "mixin" classes, each of which adds the full set of multiplication and division operations for a single combination of the wrapper and some other family of types (raw numbers, quantities, other wrappers, etc.). These wrappers use a "CRTP-ish" syntax. But instead of the _type itself_ being the first template parameter (e.g., `Wrapper<Unit>`), we provide the _wrapper_ and _unit_ as two separate parameters (i.e., `Wrapper` and `Unit`). The reason is that we need to know the `Unit`, and providing it explicitly makes it easy to get. These first two parameters are the same for all mixins, which makes the list-of-mixins at the class definition easier to read. The unit tests are based on an example, `UnitWrapper`, which aggregates a variety of properties. The forthcoming "true" classes, `Constant` and `SymbolFor`, will be defined very similarly to this. Finally, a word about the plan for "numeric" inputs. For now, we are restricting to `std::is_arithmetic`. This doesn't mean we don't support other reps; it just means we won't be able to create quantities from them via constants or symbols for a while. The evolution plan is to create a well-defined concept that defines what is a valid rep, and then replace `std::arithmetic` with that concept (see also aurora-opensource#52). Test plan: - [x] Add extensive new unit tests - [x] Manually uncomment each individual "uncomment to test" case
This is the basic machinery for representing physical constants. Later on, we plan to include `Constant` instances in the library out of the box, but first we want to explore the feature in production inside Aurora's internal repo. The first set of basic features is to multiply and divide a wide variety of types. When we do this, the operations always take place _at compile time_, and symbolically. The next set of features is various kinds of conversions to `Quantity` types and/or raw numbers. The standout feature here is the _perfect conversion policy_: since each `Constant` fully encodes its value in the type, we know exactly which `Quantity<U, R>` instances it can convert to. Doc updates are included: I added a new reference page for `Constant`. I also updated the alternatives page. A couple libraries that were previously "good" are now "fair", because they use quantities for their constants, which is sub-optimal. mp-units moved from "best" to "good" because I think on balance we're now tied. On the one hand, they have actually included pre-defined constants. On the other hand, our core implementation is better because we also support converting to `Quantity` types. Once we include constants out of the box, I expect we'll be "best". Finally, I re-alphabetized the BUILD rules. This basically meant simply moving `//au:operators` to its rightful place; not sure how it ended up way up in the "C" section in the first place. Helps aurora-opensource#90.
Specifically, we add support for `min`, `max`, `clamp`, and `%`. It turns out that the most economical way to do this is via hidden friends. The downside is that this change is invasive to `Quantity`, whereas we'd generally rather add functionality from the outside. But the upsides are that we get to remove a fair bit of extra special casing that we had done for `Zero` overloads, and even some disambiguating overloads. Not only that, but we can now support some combinations that we hadn't added before, simply because it would have been too much work! Here's how it works. The hidden friend approach covers us whenever somebody calls a function with two exactly-identical types, _or_ whenever _one_ of the types is an exact match, and the _other_ can _implicitly convert_ to it. This lets us cover all "shapeshifter types" --- `Zero`, `Constant` --- at one stroke. It even automatically covers _new shapeshifter types we don't know about_: anything implicitly convertible to `Quantity` will work! For the `min` and `max` implementation, I went with the Walter Brown approach where `min` prefers to return `a`, and `max` prefers `b`. This is the most general and correct approach w.r.t. how it handles "ties", although in our specific case this doesn't matter because we're not returning a reference. Still, I'm glad to put one more example of the Right Approach out in the wild, and I prefer it to a call to `std::min` because it doesn't force us to take a direct dependency on `<cmath>`. We have two "disambiguating" overloads remaining in `math.hh`, both applying to `QuantityPoint`: one for `min`, one for `max`. I decided not to add hidden friends there, because the cost of an invasive change, plus the cost of moving these implementations far from the other overloads in `math.hh`, outweighs the smaller benefits we would obtain in this case. Helps #90. At this point, the `Constant` _implementation_ is feature complete, and all we need to do is add concrete examples of `Constant` to our library, updating the single-file package script and documentation!
Specifically, we add support for `min`, `max`, `clamp`, and `%`. It turns out that the most economical way to do this is via hidden friends. The downside is that this change is invasive to `Quantity`, whereas we'd generally rather add functionality from the outside. But the upsides are that we get to remove a fair bit of extra special casing that we had done for `Zero` overloads, and even some disambiguating overloads. Not only that, but we can now support some combinations that we hadn't added before, simply because it would have been too much work! Here's how it works. The hidden friend approach covers us whenever somebody calls a function with two exactly-identical types, _or_ whenever _one_ of the types is an exact match, and the _other_ can _implicitly convert_ to it. This lets us cover all "shapeshifter types" --- `Zero`, `Constant` --- at one stroke. It even automatically covers _new shapeshifter types we don't know about_: anything implicitly convertible to `Quantity` will work! The one downside is that using the unqualified forms of `min`, `max`, and `clamp`, goes from "recommended" to "mandatory". We found some instances of this in Aurora's code from testing this PR; they were easily fixed by changing `au::min(...)` to `min(...)`, etc. For the `min` and `max` implementation, I went with the Walter Brown approach where `min` prefers to return `a`, and `max` prefers `b`. This is the most general and correct approach w.r.t. how it handles "ties", although in our specific case this doesn't matter because we're not returning a reference. Still, I'm glad to put one more example of the Right Approach out in the wild, and I prefer it to a call to `std::min` because it doesn't force us to take a direct dependency on `<cmath>`. We have two "disambiguating" overloads remaining in `math.hh`, both applying to `QuantityPoint`: one for `min`, one for `max`. I decided not to add hidden friends there, because the cost of an invasive change, plus the cost of moving these implementations far from the other overloads in `math.hh`, outweighs the smaller benefits we would obtain in this case. Helps #90. At this point, the `Constant` _implementation_ is feature complete, and all we need to do is add concrete examples of `Constant` to our library, updating the single-file package script and documentation!
For Au's built-in constants, we follow the exact same policies as for units, including: - A new include folder, `"au/constants/..."` - A new target, `"//au:constants"`, which globs headers from that folder - Corresponding unit tests - Inclusion in the single-file script by individual names - An `--all-constants` option for the single-file script We _don't_ provide `_fwd.hh` files, because there's nothing we could really forward declare. Constant objects are defined with spelled-out names in `ALL_CAPS` format. The corresponding file is the snake-case version. This keeps the constant itself unambiguous. We expect end users to actually use them in the following manner: ```cpp constexpr auto c = au::SPEED_OF_LIGHT; ``` Finally, we now mention new constants in the release notes. Helps #90. Remaining work includes adding more constants, and adding documentation.
For Au's built-in constants, we follow the exact same policies as for units, including: - A new include folder, `"au/constants/..."` - A new target, `"//au:constants"`, which globs headers from that folder - Corresponding unit tests - Inclusion in the single-file script by individual names - An `--all-constants` option for the single-file script Oh, and while I was updating the single-file script, I noticed a slight "bug": ever since we started providing `_fwd.hh` files for the units, the single file script was treating those files as their own units. This doesn't _hurt_ anything, but it's just a little silly (see image). This PR fixes that bug as well. ![image](https://github.com/user-attachments/assets/22381e3e-4785-4c69-8ae9-94e4ff7c7c41) We _don't_ provide `_fwd.hh` files for _constants_, because there's nothing we could really forward declare. Constant objects are defined with spelled-out names in `ALL_CAPS` format. The corresponding file is the snake-case version. This keeps the constant itself unambiguous. We expect end users to actually use them in the following manner: ```cpp constexpr auto c = au::SPEED_OF_LIGHT; ``` Finally, we now mention new constants in the release notes. Helps #90. Remaining work includes adding more constants, and adding documentation.
Turns out, using `IToA` for magnitudes was a little bit sloppy. Magnitude values are always unsigned, which means there are meaningful values that won't fit inside of an `int64_t`. Fortunately, adding all those constants (#336) was a great stress test to expose this. I think the most natural fix would be to add a `UIToA`, which can't handle signed values, but which _can_ handle the "upper half" of `uint64_t` values. This way, we can have `IToA` delegate to `UIToA` for the "integer magnitude" parts of the logic. Why not just get rid of `IToA` altogether? Well, we do need it for printing exponents, which can be negative. Helps #90: this gets the test to pass on #336.
Turns out, using `IToA` for magnitudes was a little bit sloppy. Magnitude values are always unsigned, which means there are meaningful values that won't fit inside of an `int64_t`. Fortunately, adding all those constants (#336) was a great stress test to expose this. I think the most natural fix would be to add a `UIToA`, which can't handle signed values, but which _can_ handle the "upper half" of `uint64_t` values. This way, we can have `IToA` delegate to `UIToA` for the "integer magnitude" parts of the logic. Why not just get rid of `IToA` altogether? Well, we do need it for printing exponents, which can be negative. Helps #90: this gets the test to pass on #336.
All values were taken from [this section] of the wikipedia page on the 2019 revision of the SI. Admittedly, two of them --- `Delta_nu_Cs` and `K_cd` --- have horribly awkward names. The best I could do for the constant names was `CESIUM_HYPERFINE_TRANSITION_FREQUENCY` and `LUMINOUS_EFFICACY_540_TERAHERTZ`, respectively. That's fine; we expect most users to do something like this in their programs: ```cpp constexpr auto K_cd = au::LUMINOUS_EFFICACY_540_TERAHERTZ; ``` Helps #90. [this section]: https://en.wikipedia.org/wiki/2019_revision_of_the_SI#Defining_constants
We update the installation guide, both to point users to the headers for the constants, and to tell them how to include constants in the single-file script. This made me realize that we had forgotten to make constants available in `@au//au`, so I updated the BUILD file. On the constant reference page, we now list the built-in constants. I like the way the table looks! On the monovalue types page, constants are a great example of monovalue types, so I add them as an example. We also add a how-to guide for making new constants. Finally, now that we include built-in constants with the library, I think Au's constant support is now best-in-class, so I am updating our Alternatives page to list us as such. Fixes #90.
It took me a little while to figure out exactly what I wanted to do here. Initially, I was going to _deprecate_ the standard gravity unit. Then I realized we need it to define certain other units, such as the correspondence between "pounds" as a force and a mass --- and I didn't want to introduce a circular dependency between `:units` and `:constants`. Then I was going to deprecate only the _quantity maker_, `standard_gravity`. But then I realized it would be weird to create a situation where certain units have quantity makers, and certain other ones don't. In the end, I think the right move is to just provide standard gravity as a new constant. This is the preferred way to interact with it, but the old ways will still be there for the foreseeable future. Follow-on to #90.
In some sense, we can already do this.
However, this brings
int
into the equation, via the1
. This could affect the arithmetic of the underlying Reps in some equations where it gets used. It could also run afoul of our guards against integer division.It would be nice if we could express constants solely in the realm of dimensions and magnitudes. We might need to make a new type template, such as
Constant
. It would probably be a "monovalue type" (in the nomenclature introduced in #88). Perhaps something like this:Alternatively: maybe a "constant" is just an instance of a unit? We would need to enable multiplying and dividing a quantity by a unit, if so. Then we would write:
It's interesting to imagine how this would look with other constants. Consider something like$E = mc^2$ . We might write
kilo(grams)(10.0) * squared(C)
... and the result might be labeled as10 kg * c^2
. Of course we could also apply.as(joules)
to the result to get a more familiar unit.This is an intriguing idea worth exploring more later.
The text was updated successfully, but these errors were encountered: