Skip to content

Commit

Permalink
Add compatibility utility for nholthaus units
Browse files Browse the repository at this point in the history
This creates a file, `"compatibility/nholthaus_units.hh"`, which makes
it easy to set up a correspondence between each Au Quantity type, and
the corresponding nholthaus units type.  This "correspondence" includes
bidirectional implicit conversions: so, you can pass
`::au::QuantityD<Meters>` to an API expecting
`::units::length::meter_t`, and vice versa.

This correspondence is purely opt-in.  The file also needs to be
included _after_ both of the other two libraries are included.  Since
this is fragile in general, we give instructions and an example for how
to make it _not_ fragile, in `nholthaus_units_example_usage.hh`.

We include a variety of unit tests to show that the quantities are
indeed equivalent between the libraries.  (Actually, this isn't quite
always the case.  These tests uncovered a bug in the nholthaus library,
nholthaus/units#289.)

We reserve the documentation of the corresponding-quantity feature in
general, as well as its application to the nholthaus library, for a
future PR.
  • Loading branch information
chiphogg committed Feb 20, 2023
1 parent 4e1c4a0 commit d918565
Show file tree
Hide file tree
Showing 6 changed files with 520 additions and 0 deletions.
9 changes: 9 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ http_archive(
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.14.0.tar.gz",
)

http_archive(
name = "nholthaus_units",
add_prefix = "nholthaus_units",
build_file = "@//third_party/nholthaus_units:BUILD",
sha256 = "b1f3c1dd11afa2710a179563845ce79f13ebf0c8c090d6aa68465b18bd8bd5fc",
strip_prefix = "units-2.3.3/include/",
url = "https://github.com/nholthaus/units/archive/refs/tags/v2.3.3.tar.gz",
)

load("@rules_python//python:repositories.bzl", "python_register_toolchains")

python_register_toolchains(
Expand Down
35 changes: 35 additions & 0 deletions compatibility/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2023 Aurora Operations, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

cc_library(
name = "nholthaus_units",
hdrs = ["nholthaus_units.hh"],
visibility = ["//visibility:public"],
)

cc_test(
name = "nholthaus_units_test",
size = "small",
srcs = [
"nholthaus_units_example_usage.hh",
"nholthaus_units_test.cc",
],
deps = [
":nholthaus_units",
"//au",
"//au:testing",
"@com_google_googletest//:gtest_main",
"@nholthaus_units",
],
)
252 changes: 252 additions & 0 deletions compatibility/nholthaus_units.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// Copyright 2023 Aurora Operations, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

////////////////////////////////////////////////////////////////////////////////////////////////////
// HOW TO USE THIS FILE
//
// This file does all of the "heavy lifting" in establishing correspondence between equivalent types
// in Au and in the nholthaus units library. HOWEVER, it does NOT include either of those libraries
// directly. The reason is simple: there is a wide diversity in the ways people include each of the
// libraries. Thus, if we included either directly here, it would fail to compile for many people.
//
// What you should do is to make a _new_ file, in your project, which does these things:
//
// 1. Include Au (in whatever manner is appropriate for your project).
// 2. Include the nholthaus units library (in whatever manner is appropriate for your project).
// 3. Include this present file, AFTER every other file.
//
// Relying on the order of includes is a fragile strategy in general. However, if you confine it to
// a single file, and then only ever use that new file, then the strategy can work.
////////////////////////////////////////////////////////////////////////////////////////////////////

#include <cstddef>
#include <cstdint>
#include <ratio>

namespace au {

namespace detail {

////////////////////////////////////////////////////////////////////////////////////////////////////
// This utility lets us extract a single template parameter.

template <class T>
struct SoleTemplateParameter;
template <class T>
using SoleTemplateParameterT = typename SoleTemplateParameter<T>::type;
template <template <class> class Temp, class T>
struct SoleTemplateParameter<Temp<T>> {
using type = T;
};

////////////////////////////////////////////////////////////////////////////////////////////////////
// These extract the exponent for each base unit from a coherent derived unit (which nholthaus calls
// "base_unit").

template <class U>
struct MeterExp;
template <class U>
using MeterExpT = typename MeterExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct MeterExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<M> {};

template <class U>
struct KilogramExp;
template <class U>
using KilogramExpT = typename KilogramExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct KilogramExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<Kg> {};

template <class U>
struct SecondExp;
template <class U>
using SecondExpT = typename SecondExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct SecondExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<S> {};

template <class U>
struct RadianExp;
template <class U>
using RadianExpT = typename RadianExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct RadianExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<R> {};

template <class U>
struct AmpExp;
template <class U>
using AmpExpT = typename AmpExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct AmpExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<A> {};

template <class U>
struct KelvinExp;
template <class U>
using KelvinExpT = typename KelvinExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct KelvinExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<Ke> {};

template <class U>
struct MoleExp;
template <class U>
using MoleExpT = typename MoleExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct MoleExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<Mo> {};

template <class U>
struct CandelaExp;
template <class U>
using CandelaExpT = typename CandelaExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct CandelaExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<C> {};

template <class U>
struct ByteExp;
template <class U>
using ByteExpT = typename ByteExp<U>::type;
template <class M, class Kg, class S, class R, class A, class Ke, class Mo, class C, class B>
struct ByteExp<units::base_unit<M, Kg, S, R, A, Ke, Mo, C, B>> : stdx::type_identity<B> {};

////////////////////////////////////////////////////////////////////////////////////////////////////
// These versions make sure derived units inherit their exponents from the corresponding base units.

template <class X1, class BaseUnit, class X3, class X4>
struct MeterExp<units::unit<X1, BaseUnit, X3, X4>> : MeterExp<BaseUnit> {};

template <class X1, class BaseUnit, class X3, class X4>
struct KilogramExp<units::unit<X1, BaseUnit, X3, X4>> : KilogramExp<BaseUnit> {};

template <class X1, class BaseUnit, class X3, class X4>
struct SecondExp<units::unit<X1, BaseUnit, X3, X4>> : SecondExp<BaseUnit> {};

template <class X1, class BaseUnit, class X3, class X4>
struct RadianExp<units::unit<X1, BaseUnit, X3, X4>> : RadianExp<BaseUnit> {};

template <class X1, class BaseUnit, class X3, class X4>
struct AmpExp<units::unit<X1, BaseUnit, X3, X4>> : AmpExp<BaseUnit> {};

template <class X1, class BaseUnit, class X3, class X4>
struct KelvinExp<units::unit<X1, BaseUnit, X3, X4>> : KelvinExp<BaseUnit> {};

template <class X1, class BaseUnit, class X3, class X4>
struct MoleExp<units::unit<X1, BaseUnit, X3, X4>> : MoleExp<BaseUnit> {};

template <class X1, class BaseUnit, class X3, class X4>
struct CandelaExp<units::unit<X1, BaseUnit, X3, X4>> : CandelaExp<BaseUnit> {};

template <class X1, class BaseUnit, class X3, class X4>
struct ByteExp<units::unit<X1, BaseUnit, X3, X4>> : ByteExp<BaseUnit> {};

////////////////////////////////////////////////////////////////////////////////////////////////////
// Extract the magnitude of this unit relative to a coherent combination of base units.

// `MagFromRatioT<R>` computes the Magnitude which corresponds to a given `std::ratio` instance `R`.
template <typename RatioT>
struct MagFromRatio;
template <typename RatioT>
using MagFromRatioT = typename MagFromRatio<RatioT>::type;
template <std::intmax_t N, std::intmax_t D>
struct MagFromRatio<std::ratio<N, D>> : stdx::type_identity<decltype(mag<N>() / mag<D>())> {};

// `NholthausUnitMagT<U>` is the scale factor for the nholthaus unit `U`, relative to the coherent
// combination of base units which has the same dimension.
template <class NholthausUnit>
struct NholthausUnitMag;
template <class NholthausUnit>
using NholthausUnitMagT = typename NholthausUnitMag<NholthausUnit>::type;

// Implementation for derived units: apply top-level scaling factor to recursive result.
template <class RationalScale, class BaseUnit, class PiPower, class X4>
struct NholthausUnitMag<units::unit<RationalScale, BaseUnit, PiPower, X4>>
: stdx::type_identity<MagProductT<MagFromRatioT<RationalScale>,
MagPowerT<Magnitude<Pi>, PiPower::num, PiPower::den>,
NholthausUnitMagT<BaseUnit>>> {};

// Implementation for base units: always 1 (i.e., the null Magnitude) by definition.
template <class... Es>
struct NholthausUnitMag<units::base_unit<Es...>> : stdx::type_identity<Magnitude<>> {};

////////////////////////////////////////////////////////////////////////////////////////////////////
// Compute the au unit which corresponds to a given nholthaus unit.

template <class NholthausUnit>
struct AuUnit {
using NU = NholthausUnit;

// If you want to use only a subset of units, you can avoid depending on the Au analogues for
// all 9 nholthaus base units. Simply delete the corresponding `UnitPowerT` from the
// `decltype()` expression below.
//
// **NOTE:** For safety, if you do this, make sure that you also add a line like the following
// (using moles as an example):
//
// static_assert(MoleExpT<NU>::num == 0, "Moles not supported");

using type = decltype(UnitPowerT<Meters, MeterExpT<NU>::num, MeterExpT<NU>::den>{} *
UnitPowerT<Kilo<Grams>, KilogramExpT<NU>::num, KilogramExpT<NU>::den>{} *
UnitPowerT<Seconds, SecondExpT<NU>::num, SecondExpT<NU>::den>{} *
UnitPowerT<Radians, RadianExpT<NU>::num, RadianExpT<NU>::den>{} *
UnitPowerT<Amperes, AmpExpT<NU>::num, AmpExpT<NU>::den>{} *
UnitPowerT<Kelvins, KelvinExpT<NU>::num, KelvinExpT<NU>::den>{} *
UnitPowerT<Bytes, ByteExpT<NU>::num, ByteExpT<NU>::den>{} *
UnitPowerT<Candelas, CandelaExpT<NU>::num, CandelaExpT<NU>::den>{} *
UnitPowerT<Moles, MoleExpT<NU>::num, MoleExpT<NU>::den>{} *
NholthausUnitMagT<NU>{});
};

////////////////////////////////////////////////////////////////////////////////////////////////////
// Extract the nholthaus unit from a `units::unit_t`.

template <class NholthausType>
struct NholthausUnitType;
template <class U, class R, template <class> class S>
struct NholthausUnitType<units::unit_t<U, R, S>> : stdx::type_identity<U> {};

} // namespace detail

// Define 1:1 mapping from each nholthaus type to its corresponding `au::Quantity` type.
template <class R, class RationalScale, class BaseUnit, class PiPower>
struct CorrespondingQuantity<
units::unit_t<units::unit<RationalScale, BaseUnit, PiPower, std::ratio<0>>,
R,
units::linear_scale>> {
using NholthausType = typename detail::SoleTemplateParameter<CorrespondingQuantity>::type;
using NholthausUnit = typename detail::NholthausUnitType<NholthausType>::type;

using Unit = typename detail::AuUnit<NholthausUnit>::type;
using Rep = R;

static constexpr Rep extract_value(NholthausType d) { return d.template to<Rep>(); }
static constexpr NholthausType construct_from_value(Rep x) { return NholthausType{x}; }
};

// nholthaus handles dimensionless values inconsistently, so we must work around it. See:
// https://github.com/nholthaus/units/issues/276
template <typename R>
struct CorrespondingQuantity<units::unit_t<units::concentration::percent, R, units::linear_scale>> {
using Unit = Percent;
using Rep = R;

using NholthausType = typename detail::SoleTemplateParameter<CorrespondingQuantity>::type;

// This is the workaround: we must manually multiply the value by 100, because the nholthaus
// library divides it by 100. Note the glaring asymmetry between `extract_value()` and
// `construct_from_value()`.
static constexpr Rep extract_value(NholthausType d) { return 100 * d.template to<Rep>(); }

static constexpr NholthausType construct_from_value(Rep x) { return NholthausType{x}; }
};

} // namespace au
44 changes: 44 additions & 0 deletions compatibility/nholthaus_units_example_usage.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2023 Aurora Operations, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

// clang-format off

// First, include your Au installation.
//
// For "full install" options, it will likely look like what is included here.
//
// For "single file" options, it will simply be your single file. However, make sure that it
// includes all of the following units!
#include "au/au.hh"
#include "au/units/amperes.hh"
#include "au/units/bytes.hh"
#include "au/units/candelas.hh"
#include "au/units/grams.hh"
#include "au/units/kelvins.hh"
#include "au/units/meters.hh"
#include "au/units/moles.hh"
#include "au/units/radians.hh"
#include "au/units/seconds.hh"

// Next, include your nholthaus units installation.
#include "nholthaus_units/units.h"

// Finally, include the compatibility layer.
//
// This file MUST be included AFTER all others.
#include "compatibility/nholthaus_units.hh"

// clang-format on
Loading

0 comments on commit d918565

Please sign in to comment.