diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 260fd0f8..c51ffe6f 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -70,10 +70,12 @@ add_custom_command(OUTPUT "${SPHINX_INDEX_FILE}" "${CMAKE_CURRENT_SOURCE_DIR}/CHANGELOG.md" "${CMAKE_CURRENT_SOURCE_DIR}/design.rst" "${CMAKE_CURRENT_SOURCE_DIR}/design/directories.rst" + "${CMAKE_CURRENT_SOURCE_DIR}/design/downcasting.rst" "${CMAKE_CURRENT_SOURCE_DIR}/design/quantity.rst" "${CMAKE_CURRENT_SOURCE_DIR}/examples.rst" - "${CMAKE_CURRENT_SOURCE_DIR}/examples/hello_units.rst" "${CMAKE_CURRENT_SOURCE_DIR}/examples/avg_speed.rst" + "${CMAKE_CURRENT_SOURCE_DIR}/examples/box_example.rst" + "${CMAKE_CURRENT_SOURCE_DIR}/examples/hello_units.rst" "${CMAKE_CURRENT_SOURCE_DIR}/examples/linear_algebra.rst" "${CMAKE_CURRENT_SOURCE_DIR}/examples/measurement.rst" "${CMAKE_CURRENT_SOURCE_DIR}/faq.rst" @@ -100,7 +102,7 @@ add_custom_command(OUTPUT "${SPHINX_INDEX_FILE}" "${CMAKE_CURRENT_SOURCE_DIR}/use_cases/legacy_interfaces.rst" "${CMAKE_CURRENT_SOURCE_DIR}/use_cases/linear_algebra.rst" "${CMAKE_CURRENT_SOURCE_DIR}/use_cases/natural_units.rst" - "${CMAKE_CURRENT_SOURCE_DIR}/use_cases/unknown_units_and_dimensions.rst" + "${CMAKE_CURRENT_SOURCE_DIR}/use_cases/unknown_dimensions.rst" "${CMAKE_CURRENT_SOURCE_DIR}/usage.rst" "${DOXYGEN_INDEX_FILE}" MAIN_DEPENDENCY "${SPHINX_SOURCE}/conf.py" diff --git a/docs/DESIGN.md b/docs/DESIGN.md deleted file mode 100644 index 9009f346..00000000 --- a/docs/DESIGN.md +++ /dev/null @@ -1,965 +0,0 @@ -# `mp-units` - Design Overview - - -## Summary - -`mp-units` is a compile-time enabled Modern C++ library that provides compile-time dimensional -analysis and unit/quantity manipulation. The basic idea and design heavily bases on -`std::chrono::duration` and extends it to work properly with many dimensions. - -Here is a small example of possible operations: - -```cpp -// simple numeric operations -static_assert(10q_km / 2 == 5q_km); - -// unit conversions -static_assert(1q_h == 3600q_s); -static_assert(1q_km + 1q_m == 1001q_m); - -// dimension conversions -static_assert(1q_km / 1q_s == 1000q_m_per_s); -static_assert(2q_km_per_h * 2q_h == 4q_km); -static_assert(2q_km / 2q_km_per_h == 1q_h); - -static_assert(1000 / 1q_s == 1q_kHz); - -static_assert(10q_km / 5q_km == 2); -``` - - -## Approach - -1. Safety and performance - - strong types - - compile-time safety - - `constexpr` all the things - - as fast or even faster than when working with fundamental types -2. The best possible user experience - - compiler errors - - debugging -3. No macros in the user interface -4. Easy extensibility -5. No external dependencies -6. Possibility to be standardized as a freestanding part of the C++ Standard Library - - -## Basic Concepts - -The most important concepts in the library are `Unit`, `Dimension`, and `Quantity`: - -![Design UML](_static/img/concepts.png) - -`Unit` is a basic building block of the library. Every dimension works with a concrete -hierarchy of units. Such hierarchy defines a reference unit and often a few scaled versions of -it. - -`Dimension` concept matches a dimension of either a base or derived quantity. `base_dimension` -is instantiated with a unique symbol identifier and a base unit. `derived_unit` is a list of -exponents of either base or other derived dimensions. - -`Quantity` is a concrete amount of a unit for a specified dimension with a specific -representation. - - -## `Unit` - -All units are represented in the framework by a `scaled_unit` class template: - -```cpp -template -struct scaled_unit : downcast_base> { - using ratio = R; - using reference = U; -}; -``` - -where: - -```cpp -template -concept UnitRatio = Ratio && R::num > 0 && R::den > 0; // double negatives not allowed -``` - -and `Ratio` is satisfied by any instantiation of `units::ratio`. - -The `scaled_unit` type is a framework's private type and the user should never instantiate it directly. -The public user interface to create units consists of: - -![Units UML](_static/img/units.png) - -All below class templates indirectly derive from a `scaled_unit` class template and satisfy a -`Unit` concept: -- `unit` - - Defines a new unnamed, in most cases coherent derived unit of a specific derived - dimension and it should be passed in this dimension's definition. -- `named_unit` - - Defines a named, in most cases base or coherent unit that is then passed to a dimension's - definition. - - A named unit may be used by other units defined with the prefix of the same type, unless - `no_prefix` is provided for `PrefixFamily` template parameter (in such a case it is impossible - to define a prefixed unit based on this one). -- `named_scaled_unit` - - Defines a new named unit that is a scaled version of another unit. - - Such unit can be used by other units defined with the prefix of the same type, unless - `no_prefix` is provided for `PrefixFamily` template parameter (in such a case it is impossible - to define a prefixed unit based on this one). -- `prefixed_unit` - - Defines a new unit that is a scaled version of another unit by the provided prefix. - - It is only possible to create such a unit if the given prefix type matches the one defined - in a reference unit. -- `deduced_unit` - - Defines a new unit with a deduced ratio and symbol based on the recipe from the provided - derived dimension. - - The number and order of provided units should match the recipe of the derived dimension. - - All of the units provided should also be a named ones so it is possible to create a deduced - symbol text. - -Some of the above types depend on `PrefixFamily` and `no_prefix`. `PrefixFamily` is a concept that -is defined as: - -```cpp -template -concept PrefixFamily = std::derived_from; -``` - -where `prefix_family` is just an empty tag type used to identify the beginning of prefix types -hierarchy and `no_prefix` is one of its children: - -```cpp -struct prefix_family {}; -struct no_prefix : prefix_family {}; -``` - -Concrete prefix derives from a `prefix` class template: - -```cpp -template - requires (!std::same_as) -struct prefix; -``` - -You could notice that both units and above `prefix` class template take `Child` as a first -template parameter. `mp-units` library heavily relies on CRTP (Curiously Recurring Template -Parameter) idiom to provide the best user experience in terms of readability of compilation -errors and during debugging. It is possible thanks to the downcasting facility described later -in the design documentation. - -Coming back to units, here are a few examples of unit definitions: - -```cpp -namespace units::physical::si { - -// prefixes -struct prefix : prefix_family {}; -struct centi : units::prefix> {}; -struct kilo : units::prefix> {}; - -// length -struct metre : named_unit {}; -struct centimetre : prefixed_unit {}; -struct kilometre : prefixed_unit {}; - -// time -struct second : named_unit {}; -struct hour : named_scaled_unit, second> {}; - -// speed -struct metre_per_second : unit {}; -struct kilometre_per_hour : deduced_unit {}; - -} - -namespace units::physical::us { - -// length -struct yard : named_scaled_unit, si::metre> {}; -struct mile : named_scaled_unit, yard> {}; - -// speed -struct mile_per_hour : deduced_unit {}; - -} -``` - -Please note that thanks to C++20 features we are able to provide all information about the unit -(including text output) in a single line of its type definition. There is no need to specialize -additional type traits or use preprocessor macros. - - -## `Dimension` - -`Dimension` is either a `BaseDimension` or a `DerivedDimension`: - -```cpp -template -concept Dimension = BaseDimension || DerivedDimension; -``` - -### `BaseDimension` - -According to ISO 80000 a base quantity is a quantity in a conventionally chosen subset of a -given system of quantities, where no quantity in the subset can be expressed in terms of the -other quantities within that subset. They are referred to as being mutually independent since a -base quantity cannot be expressed as a product of powers of the other base quantities. Base unit -is a measurement unit that is adopted by convention for a base quantity in a specific system of -units. - -`base_dimension` represents a dimension of a base quantity and is identified with a pair of -an unique compile-time text describing the dimension symbol and a base unit adopted for this -dimension: - -```cpp -template - requires U::is_named -struct base_dimension { - static constexpr auto symbol = Symbol; - using base_unit = U; -}; -``` - -Pair of symbol and unit template parameters form an unique identifier of the base dimension. -These identifiers provide total ordering of exponents of base dimensions in a derived dimension. - -The SI physical units system defines 7 base dimensions: - -```cpp -namespace units::physical::si { - -struct dim_length : base_dimension<"L", metre> {}; -struct dim_mass : base_dimension<"M", kilogram> {}; -struct dim_time : base_dimension<"T", second> {}; -struct dim_electric_current : base_dimension<"I", ampere> {}; -struct dim_thermodynamic_temperature : base_dimension<"Θ", kelvin> {}; -struct dim_substance : base_dimension<"N", mole> {}; -struct dim_luminous_intensity : base_dimension<"J", candela> {}; - -} -``` - -All other derived quantities of SI are composed from those. - -There are two reasons why a `base_dimension` gets a unit as its template parameter. First, the -base unit is needed for the text output of unnamed derived units. Second, there is more than -one system of physical units. For example CGS definitions look as follows: - -```cpp -namespace units::physical::cgs { - -using si::centimetre; -using si::gram; -using si::second; - -struct dim_length : base_dimension<"L", centimetre> {}; -struct dim_mass : base_dimension<"M", gram> {}; -using si::dim_time; - -} -``` - -Equivalent base dimensions in different systems have the same symbol identifier and get units -from the same hierarchy (with the same reference in `scaled_unit`). Thanks to that we have -the ability to explicitly cast quantities of the same dimension from different systems or -even mix them in one `derived_dimension` definition. - - -### `DerivedDimension` - -According to ISO 80000 a derived quantity is a quantity, in a system of quantities, defined in -terms of the base quantities of that system. Dimension of such quantity is an expression of the -dependence of a quantity on the base quantities of a system of quantities as a product of -powers of factors corresponding to the base quantities, omitting any numerical factors. A power -of a factor is the factor raised to an exponent. Each factor is the dimension of a base -quantity. - -A derived dimension used internally in a library framework is implemented as a type-list like -type that stores an ordered list of exponents of one or more base dimensions: - -```cpp -namespace detail { - -template - requires (BaseDimension && ... && BaseDimension) -struct derived_dimension_base; - -} -``` - -A derived dimension can be formed from multiple exponents (i.e. speed is represented as -`exp, exp`). It is also possible to form a derived dimension with only one exponent -(i.e. frequency is represented as just `exp`). - -Exponents are implemented with `exp` class template that provides an information about a single -dimension and its (possibly fractional) exponent in a derived dimension. - -```cpp -template -struct exp { - using dimension = Dim; - static constexpr std::intmax_t num = Num; - static constexpr std::intmax_t den = Den; -}; -``` - -In order to be able to perform computations on an arbitrary set of exponents, -`derived_dimension_base` class template have to obey the following rules: -- it contains only base dimensions in the list of exponents, -- base dimensions are not repeated in a list (the exponent of each base dimension is provided - at most once), -- exponents of base dimensions are consistently ordered, -- in case the numerator of the exponent equals zero such base dimension is erased from the list. - -Above is needed for the framework to provide dimensional analysis. However, sometimes it is -useful to define derived units in terms of other derived units. To support this both a base -dimension and a derived dimension can be provided to `exp` class template. - -As it was stated above `derived_dimension_base` is a private utility of the framework. In order -to define a new derived dimension the user has to instantiate the following class template: - -```cpp -template -struct derived_dimension : downcast_child> { - using recipe = exp_list; - using coherent_unit = U; - using base_units_ratio = /* see below */; -}; -``` - -There are a few important differences between `detail::derived_dimension_base` and -`derived_dimension`. First, the latter one gets the coherent unit of the derived dimension. - -According to ISO 80000 a coherent unit is a unit that, for a given system of quantities and for -a chosen set of base units, is a product of powers of base units with no other proportionality -factor than one. - -The other difference is that `derived_dimension` allows to provide other derived dimensions in -the list of its exponents. This is called a "recipe" of the dimension and among others is used -to print unnamed coherent units of this dimension. - -In case a derived dimension appears on the list of exponents, such derived dimension will be -unpacked, sorted, and consolidated by a `detail::make_dimension` helper to form a valid list -of exponents of only base dimensions later provided to `detail::derived_dimension_base`. - -Sometimes units of equivalent quantities in different systems of units do not share the same -reference so they cannot be easily converted to each other. An example can be a pressure for -which a coherent unit in SI is pascal and in CGS barye. Those two units are not directly -related with each other with some ratio. As they both are coherent units of their dimensions, -the ratio between them is directly determined by the ratios of base units defined in base -dimensions end their exponents in the derived dimension recipe. To provide interoperability of -such quantities of different systems `base_units_ratio` is being used. The result of the -division of two `base_units_ratio` of two quantities of equivalent dimensions in two different -systems gives a ratio between their coherent units. Alternatively, the user would always have to -directly define a barye in terms of pascal or vice versa. - -Below are a few examples of derived dimension definitions: - -```cpp -namespace units::physical::si { - -struct dim_velocity : derived_dimension, exp> {}; - -struct dim_acceleration : derived_dimension, exp> {}; - -struct dim_force : derived_dimension, exp> {}; - -struct dim_energy : derived_dimension, exp> {}; - -struct dim_power : derived_dimension, exp> {}; - -} -``` - -If as a result of dimensional computation the library framework will generate a derived -dimension that was not predefined by the user than the instance of -`unknown_dimension`. The coherent unit of such an unknown dimension is -`scaled_unit, unknown_coherent_unit>`. - - -## `Quantity` - -`quantity` is a class template that expresses the quantity/amount of a specific dimension -expressed in a specific unit of that dimension: - -```cpp -template U, Scalar Rep = double> -class quantity -``` - -`quantity` provides a similar interface to `std::chrono::duration`. The difference is that it -uses `double` as a default representation and has a few additional member types and -functions as below: - -```cpp -template U, Scalar Rep = double> -class quantity { -public: - using dimension = D; - using unit = U; - using rep = Rep; - - [[nodiscard]] static constexpr quantity one() noexcept; - // ... -}; - -template - requires detail::basic_arithmetic && equivalent_dim> -[[nodiscard]] constexpr Scalar auto operator*(const quantity& lhs, - const quantity& rhs); - -template - requires detail::basic_arithmetic && (!equivalent_dim>) -[[nodiscard]] constexpr Quantity auto operator*(const quantity& lhs, - const quantity& rhs); - -template - requires std::magma -[[nodiscard]] constexpr Quantity auto operator/(const Value& v, - const quantity& q); - -template - requires detail::basic_arithmetic && equivalent_dim -[[nodiscard]] constexpr Scalar auto operator/(const quantity& lhs, - const quantity& rhs); - -template - requires detail::basic_arithmetic && (!equivalent_dim) -[[nodiscard]] constexpr Quantity AUTO operator/(const quantity& lhs, - const quantity& rhs); -``` - -Additional functions provide the support for operations that result in a different dimension -type than those of their arguments. `equivalent_dim` constraint requires two dimensions to be -either the same or have convertible units of base dimension (with the same reference unit). - -Beside adding new elements a few other changes where applied compared to the `std::chrono::duration` class: -1. The `duration` is using `std::common_type_t` to find a common representation - for a calculation result. Such a design was reported as problematic by SG6 (numerics study group) members - as sometimes we want to provide a different type in case of multiplication and different in case of - division. `std::common_type` lacks that additional information. That is why `units::quantity` uses - the resulting type of a concrete operator operation. -2. `operator %` is constrained with `treat_as_floating_point` type trait to limit the types to integral - representations only. Also `operator %(Rep)` takes `Rep` as a template argument to limit implicit - conversions. - -To simplify writing efficient generic code quantities of each dimension have associated: -1. Concept (i.e. `units::Length`) that matches a length dimension of any physical systems. -2. Per-system quantity alias (i.e. `units::physical::si::length` for - `units::quantity`). - -Also, to help instantiate quantities with compile-time known values every unit in the library -has an associated UDL. For example: - -```cpp -namespace si::inline literals { - -// m -constexpr auto operator"" q_m(unsigned long long l) { return length(l); } -constexpr auto operator"" q_m(long double l) { return length(l); } - -// km -constexpr auto operator"" q_km(unsigned long long l) { return length(l); } -constexpr auto operator"" q_km(long double l) { return length(l); } - -} -``` - - -### `quantity_cast` - -To explicitly force truncating conversions `quantity_cast` function is provided which is a direct -counterpart of `std::chrono::duration_cast`. As a template argument user can provide here either -a `quantity` type or only its template parameters (`Dimension`, `Unit`, or `Rep`): - -```cpp -template - requires QuantityOf && - detail::basic_arithmetic> -[[nodiscard]] constexpr auto quantity_cast(const quantity& q); - -template - requires equivalent_dim -[[nodiscard]] constexpr auto quantity_cast(const quantity& q); - -template - requires UnitOf -[[nodiscard]] constexpr auto quantity_cast(const quantity& q); - -template - requires detail::basic_arithmetic> -[[nodiscard]] constexpr auto quantity_cast(const quantity& q); -``` - -## Text output - -### Unit Symbol - -The library tries its best to print a correct unit of the quantity. This is why it performs -a series of checks: -1. If the user predefined a unit with a `named_XXX_unit` class templates, the symbol provided - by the user will be used (i.e. `60 W`). -2. If a unit was created with a `deduced_unit` class template, the symbol of deduced unit is - printed (i.e. `70 km/h`). -3. Otherwise, the library tries to print a prefix and symbol of an unknown unit for this derived - dimension: - - prefix: - - if ratio of the scaled unit is `1`, than no prefix is being printed, - - otherwise, if `PrefixFamily` template parameter of a reference unit is different than - `no_prefix`, and if the ratio of scaled unit matches the ratio of a prefix of a specified - type, than the symbol of this prefix will be used, - - otherwise, non-standard ratio (i.e. `2 [60]Hz`) will be printed. - - symbol: - - if a reference unit has a user-predefined or deduced symbol, than this symbol it is being - printed, - - otherwise, the symbol is constructed from names and exponents of base dimensions - (i.e. `2 m/kg^2`). - - -### `operator<<` - -`quantity::operator<<()` provides only a basic support to print a quantity. It prints its count -and a symbol separated with one space character. - - -### Text Formatting - -`mp-units` supports new C++20 formatting facility (currently provided as a dependency on -[`fmt`](https://github.com/fmtlib/fmt) library). `parse()` member functions of -`fmt::formatter, CharT>` class template partial -specialization interprets the format specification as a `units-format-spec` according to the -following syntax: - -```text -units-format-spec: - fill-and-align[opt] sign[opt] width[opt] precision[opt] units-specs[opt] -units-specs: - conversion-spec - units-specs conversion-spec - units-specs literal-char -literal-char: - any character other than { or } -conversion-spec: - % modifier[opt] type -modifier: one of - E O -type: one of - n q Q t % -``` - -The productions `fill-and-align`, `sign`, `width`, and `precision` are described in -[Format string](https://wg21.link/format.string.std) chapter of the C++ standard. Giving a -`precision` specification in the `units-format-spec` is valid only for `units::quantity` types -where the representation type `Rep` is a floating-point type. For all other `Rep` types, an -exception of type `format_error` is thrown if the `units-format-spec` contains a precision -specification. An `format_error` is also thrown if `sign` is provided with a `conversion-spec` -to print quantity unit but not its value. - -Each conversion specifier `conversion-spec` is replaced by appropriate characters as described -in the following table: - -| Specifier | Replacement | -|:---------:|---------------------------------------------------------------| -| `%n` | A new-line character | -| `%q` | The quantity’s unit symbol | -| `%Q` | The quantity’s numeric value (as if extracted via `.count()`) | -| `%t` | A horizontal-tab character | -| `%%` | A `%` character | - -If the `units-specs` is omitted, the `quantity` object is formatted as if by streaming it to -`std::ostringstream os` and copying `os.str()` through the output iterator of the context with -additional padding and adjustments as specified by the format specifiers. - -```cpp -std::string s = fmt::format("{:=>12}", 120q_km_per_h); // value of s is "====120 km/h" -``` - - -## Improving user's experience - -Most of the important design decisions in the library are dictated by the requirement of -providing the best user experience as possible. - -Most of C++ libraries in the world use template aliases to provide a friendly name for a -developer. Unfortunately, such aliases are quickly lost in a compilation process and as a -result the potential error log contains a huge source type rather than a short alias for it. -The same can be observed during debugging of a code using template aliases. - -Let's assume that we want to provide a user friendly name for a capacitance derived dimension. -Other libraries will do it in the following way: - -```cpp -using dim_capacitance = detail::derived_dimension_base, - exp, - exp, - exp>; -``` - -The above solution does provide a good developer's experience but a really poor one for the end -user. If we will get a compilation error message containing `dim_capacitance` in most cases -the compiler will print the following type instead of the alias: - -```text -units::detail::derived_dimension_base, -units::exp, units::exp, -units::exp > -``` - -You can notice that even this long syntax was carefully selected to provide quite good user -experience (some other units libraries produce a type that cannot easily fit on one slide) -but it is not questionable less readable than just `dim_capacitance`. - -NOTE: To better understand how the framework works and not clutter the text and graphs with -long types in the following examples we will switch from `dim_capacitance` to `dim_area`. -The latter one has much shorter definition but the end result for both will be exactly the same. -User-friendly, short name printed by the compiler and the debugger. - -To fix it we have to provide a strong type. As we do not have opaque/strong typedefs -in the language we have to use inheritance: - -![UML](_static/img/downcast_1.png) - -This gives us a nice looking strong type but does not solve the problem of how to switch from -a long instantiation of a `derived_dimension_base` class template that was generated by the -framework as a result of dimensional calculation to a child class assigned by the user for this -instantiation. - -### Downcasting facility - -To support this `mp-units` library introduces a new downcasting facility implemented fully as -a library feature. It creates 1-to-1 link between a long class template instantiation and a -strong type provided by the user. This means that only one child class can be created for a -specific base class template instantiation. - -Downcasting facility is provided by injecting two classes into our hierarchy: - -![UML](_static/img/downcast_2.png) - -In the above example `dim_area` is a downcasting target (child class) and a specific -`detail::derived_dimension` class template instantiation is a downcasting source (base class). - -```cpp -template -struct downcast_base { - using downcast_base_type = BaseType; - friend auto downcast_guide(downcast_base); // declaration only (no implementation) -}; -``` - -`units::downcast_base` is a class that implements CRTP idiom, marks the base of downcasting -facility with a `downcast_base_type` member type, and provides a declaration of downcasting ADL -friendly (Hidden Friend) entry point member function `downcast_guide`. An important design point -is that this function does not return any specific type in its declaration. This non-member -function is going to be defined in a child class template `downcast_child` and will return a -target type of the downcasting operation there. - -```cpp -template -concept Downcastable = - requires { - typename T::downcast_base_type; - } && - std::derived_from>; -``` - -`units::Downcastable` is a concepts that verifies if a type implements and can be used in a -downcasting facility. - -```cpp -template -struct downcast_child : T { - friend auto downcast_guide(typename downcast_child::downcast_base) { return Target(); } -}; -``` - -`units::downcast_child` is another CRTP class template that provides the implementation of a -non-member friend function of the `downcast_base` class template which defines the target -type of a downcasting operation. - -With such CRTP types the only thing the user has to do to register a new type to the downcasting -facility is to publicly derive from one of those CRTP types and provide its new child type as -the first template parameter of the CRTP type. - -Above types are used to define base and target of a downcasting operation. To perform the actual -downcasting operation a dedicated template alias is provided: - -```cpp -template -using downcast = decltype(detail::downcast_target_impl()); -``` - -`units::downcast` is used to obtain the target type of the downcasting operation registered -for a given instantiation in a base type. `detail::downcast_target_impl` checks if a downcasting -target is registered for the specific base class. If yes, it returns the registered type, -otherwise it works like a regular identity type returning a provided base class. - -```cpp -namespace detail { - - template - concept has_downcast = requires { - downcast_guide(std::declval>()); - }; - - template - constexpr auto downcast_target_impl() - { - if constexpr(has_downcast) - return decltype(downcast_guide(std::declval>()))(); - else - return T(); - } - -} -``` - -Additionally there is one more simple helper alias provided that is used in the internal -library implementation: - -```cpp -template -using downcast_base_t = T::downcast_base_type; -``` - - -### `unknown_dimension` - -Sometimes dimensional calculation results with a class template instantiation that was not -predefined by the user in the downcasting facility. A typical example of such a case are -temporary results of calculations: - -```cpp -units::Length auto d1 = 123q_m; -units::Time auto t1 = 10q_s; -units::Speed auto v1 = avg_speed(d1, t1); - -auto temp1 = v1 * 50q_m; // intermediate unknown dimension - -units::Speed auto v2 = temp1 / 100q_m; // back to known dimensions again -units::Length auto d2 = v2 * 60q_s; -``` - -To provide support to form an unknown derived dimension that could be than be converted to a -known one with a correct unit, and also to improve the user experience and clearly state that -it is an unknown dimension the library framework will provide an instance of: - -```cpp -struct unknown_coherent_unit : unit {}; - -template -struct unknown_dimension : derived_dimension, - scaled_unit, unknown_coherent_unit>, - E, ERest...> { - using coherent_unit = scaled_unit, unknown_coherent_unit>; -}; -``` - -with this the error log or a debugger breakpoint involving a `temp1` type will include: - -```text -units::quantity, -units::exp >, units::unknown_coherent_unit, long int> -``` - - -## Extensibility - -The library was designed with a simple extensibility in mind. It is easy to add new units, -dimensions, and prefixes. The systems of units are not closed (classes) but open (namespaces) -and can be easily extended, or its content can be partially/fully imported to other systems. - - -### Adding a new system with custom dimensions and units - -A great example of a adding a whole new system can be a `data` system in the library which -adds support for digital information quantities. In summary it adds: -1. New prefix type and its prefixes: - - ```cpp - namespace units::data { - - struct prefix : prefix_family {}; - - struct kibi : units::prefix> {}; - struct mebi : units::prefix> {}; - - } - ``` - -2. New units for `information`: - - ```cpp - namespace units::data { - - struct bit : named_unit {}; - struct kibibit : prefixed_unit {}; - - struct byte : named_scaled_unit, bit> {}; - struct kibibyte : prefixed_unit {}; - - } - ``` - -3. New base dimension, its concept, and quantity alias: - - ```cpp - namespace units::data { - - struct dim_information : base_dimension<"information", bit> {}; - - template - concept Information = QuantityOf; - - template - using information = quantity; - - } - ``` - -4. UDLs for new units - - ```cpp - namespace units::data::inline literals { - - // bits - constexpr auto operator"" q_b(unsigned long long l) { return information(l); } - constexpr auto operator"" q_Kib(unsigned long long l) { return information(l); } - - // bytes - constexpr auto operator"" q_B(unsigned long long l) { return information(l); } - constexpr auto operator"" q_KiB(unsigned long long l) { return information(l); } - - } - ``` - -5. A new `bitrate` derived dimension, its units, concept, quantity helper, and UDLs - - ```cpp - namespace units::data { - - struct bit_per_second : unit {}; - struct dim_bitrate : derived_dimension, exp> {}; - - struct kibibit_per_second : deduced_unit {}; - - template - concept Bitrate = QuantityOf; - - template - using bitrate = quantity; - - inline namespace literals { - - // bits - constexpr auto operator"" q_b_per_s(unsigned long long l) { return bitrate(l); } - constexpr auto operator"" q_Kib_per_s(unsigned long long l) { return bitrate(l); } - - } - - } - ``` - - -### Using custom representations - -In theory `quantity` can take any arithmetic-like type as a `Rep` template parameter. In -practice some interface is forced by numeric concepts. - -To provide basic library functionality the type should satisfy the `Scalar` concept: - -```cpp -template -concept basic-arithmetic = // exposition only - std::magma && - std::magma && - std::magma && - std::magma; - -template -concept Scalar = - (!Quantity) && - (!WrappedQuantity) && - std::regular && - std::totally_ordered && - basic-arithmetic; -``` - -Where `WrappedQuantity` is a concept that applies `Quantity` recursively -on all nested types to check if `T` is not actually a wrapped quantity type (i.e. a vector or -matrix of quantities). - -The above implies that the `Rep` type should provide at least: -- default constructor, destructor, copy-constructor, and copy-assignment operator -- `operator==(Rep, Rep)`, `operator!=(Rep, Rep)` -- `operator<(Rep, Rep)`, `operator>(Rep, Rep)`, `operator<=(Rep, Rep)`, `operator>=(Rep, Rep)` -- `operator-(Rep)` -- `operator+(Rep, Rep)`, `operator-(Rep, Rep)`, `operator*(Rep, Rep)`, `operator*(Rep, Rep)` - -Above also requires that the `Rep` should be implicitly convertible from integral types -(i.e. `int`) so a proper implicit converting constructor should be provided. - -Moreover, in most cases to observe expected behavior `Rep` will have to be registered as a -floating-point representation type by specializing `units::treat_as_floating_point` type -trait: - -```cpp -template -inline constexpr bool treat_as_floating_point; -``` - -An example of such a type can be found in [measurement example](../example/measurement.cpp). - -However, as written above this will enable only a basic functionality of the library. In case -additional `quantity` operations are needed the user may opt-in to any of them by providing -the equivalent operation for `Rep` type. Here is an additional list of opt-in operations: -- `operator++()` -- `operator++(int)` -- `operator--()` -- `operator--(int)` -- `operator+=(Rep)` -- `operator-=(Rep)` -- `operator*=(Rep)` -- `operator/=(Rep)` -- `operator%=(Rep)` -- `operator%(Rep, Rep)` - -`quantity` also has 4 static functions `zero()`, `one()`, `min()`, and `max()` which can -be enabled by providing a specialization of `quantity_values` type trait for `Rep` type: - -```cpp -template -struct quantity_values; -``` - -## FAQ - -1. Why all UDLs are prefixed with `q_` instead of just using unit symbol? - - Usage of only unit symbols in UDLs would be a preferred approach (less to type, easier to - understand and maintain). However, while increasing the coverage for the library we learned - that there are a lot unit symbols that conflict with built-in types or numeric extensions. - A few of those are: `F` (farad), `J` (joule), `W` (watt), `K` (kelvin), `d` (day), `l` or - `L` (litre), `erg`, `ergps`. For a while we had to used `_` prefix to make the library work - at all but at some point we had to unify the naming and we came up with `q_` prefix which - results in a creation of quantity of a provided unit. - -2. Why dimensions depend on units and not vice versa? - - Most of the libraries define units in terms of dimensions and this was also an initial - approach for this library. However it turns out that for such a design it is hard to provide - support for all the required scenarios. - - The first of them is to support multiple unit systems (like SI, CGS, ...) where each of - can have a different base unit for the same dimension. Base quantity of dimension length in - SI has to know that it should use `m` to print the unit symbol to the text output, while - the same dimension for CGS should use `cm`. Also it helps in conversions among those systems. - - The second one is to support natural units where more than one dimension can be measured - with the same unit (i.e. `GeV`). Also if someone will decide to implement a systems where - SI quantities of the same kind are expressed as different dimensions (i.e. height, width, - and depth) all of them will just be measured in meters. - -3. Why do we spell `metre` instead of `meter`? diff --git a/docs/design.rst b/docs/design.rst index d3dc9231..1f79773d 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -13,19 +13,4 @@ Design Deep Dive design/directories design/quantity - -The Downcasting Facility ------------------------- - -.. - http://www.nomnoml.com - - [detail::derived_dimension_base>]<:-[dim_area] - - -.. - http://www.nomnoml.com - - [downcast_base>>]<:-[detail::derived_dimension_base>] - [detail::derived_dimension_base>]<:-[downcast_child>>] - [downcast_child>>]<:-[dim_area] + design/downcasting diff --git a/docs/design/downcasting.rst b/docs/design/downcasting.rst new file mode 100644 index 00000000..e5173482 --- /dev/null +++ b/docs/design/downcasting.rst @@ -0,0 +1,163 @@ +.. namespace:: units + +The Downcasting Facility +======================== + +Problem statement +----------------- + +Most of the C++ libraries in the world use template aliases to provide a friendly name for a +developer. Unfortunately, such aliases are quickly lost in a compilation process and as a +result the potential error log contains a huge source type rather than a short alias for it. +The same can be observed during debugging of a source code that use template aliases. + +Let's assume that we want to provide a user friendly name for a derived dimension of capacitance +quantity. Other libraries will do it in the following way:: + + using dim_capacitance = detail::derived_dimension_base, + exp, + exp, + exp>; + +The above solution does provide a good developer's experience but a really poor one for the end +user. If we will get a compilation error message containing `dim_capacitance` in most cases +the compiler will print the following type instead of the alias:: + + units::detail::derived_dimension_base, + units::exp, units::exp, + units::exp > + +You can notice that in case of **mp-units** even this long syntax was carefully selected to +provide quite good user experience (some other units libraries produce a type that cannot easily +fit on one slide) but it is not questionable less readable than just `dim_capacitance`. + +.. note:: + + To better understand how the framework works and not clutter the text and graphs with + long types in the following examples we will switch from `dim_capacitance` to `dim_area`. + The latter one has much shorter definition but the end result for both will be exactly the same - + user-friendly, short name printed by the compiler and the debugger. + + +As we lack opaque/strong typedefs in the C++ language the only way to improve our case is +to use inheritance: + +.. image:: /_static/img/downcast_1.png + :align: center + +.. + http://www.nomnoml.com + + [derived_dimension_base>]<:-[dim_area] + +This gives us a nice looking strong type when directly used by the user. However, we just got +ourselves into problems. The library's framework does not know how to switch from a long +instantiation of a `derived_dimension_base` class template that was generated as a result +of dimensional calculation to a nicely named child class assigned by the user for this +instantiation. + + +How it works? +------------- + +To support this **mp-units** library introduces a new downcasting facility implemented fully +as a library feature. It creates 1-to-1 link between a long class template instantiation and a +strong type provided by the user. This provides automatic type substitution mechanism in the +framework. + +.. important:: + + The above 1-1 correspondence means that only one child class can be provided for a specific + base class template instantiation. If a user will try to assign another child class to + already used base class template instantiation the program will not compile. + +The downcasting facility is provided by injecting two classes into our hierarchy: + +.. image:: /_static/img/downcast_2.png + :align: center + +.. + http://www.nomnoml.com + + [downcast_base>>]<:-[detail::derived_dimension_base>] + [detail::derived_dimension_base>]<:-[downcast_child>>] + [downcast_child>>]<:-[dim_area] + +In the above example: + +- ``dim_area`` is a downcasting target (child class) + +- `detail::derived_dimension_base` class template instantiation is a downcasting source (base class) + +- `downcast_base` is a class that implements :abbr:`CRTP (Curiously Recurring Template Pattern)` + idiom, stores the base of a downcasting operation in a ``downcast_base_type`` member type, + and provides only a Hidden Friend non-member function declaration of ``downcast_guide`` which is an + :abbr:`ADL (Argument Dependent Lookup)` entry point for the downcasting operation:: + + template + struct downcast_base { + using downcast_base_type = BaseType; + friend auto downcast_guide(downcast_base); // declaration only (no implementation) + }; + + .. important:: + + An important design point here is that this friend function does not return any specific type + in its declaration and no definition is provided at this point. + +- `downcast_child` is another :abbr:`CRTP (Curiously Recurring Template Pattern)` class template + that defines the implementation of a non-member friend function of the `downcast_base` class + template:: + + template + struct downcast_child : T { + friend auto downcast_guide(typename downcast_child::downcast_base) { return Target(); } + }; + + This is the place where the actual return type of the ``downcast_guide`` function is provided + which serves as a target type of the downcasting operation. + + In the above class template definition `Downcastable` is a concepts that verifies if a type + implements and can be used in a downcasting facility:: + + template + concept Downcastable = + requires { + typename T::downcast_base_type; + } && + std::derived_from>; + + +With such :abbr:`CRTP (Curiously Recurring Template Pattern)` types the only thing the user +has to do in order to register a new type in the downcasting facility is to publicly derive +from `downcast_child` and pass this type as the first template argument of the `downcast_child` +class template. + +Until now we scoped on how we define the base and target of a downcasting operation. To +perform the actual downcasting operation a dedicated alias template is provided:: + + template + using downcast = decltype(detail::downcast_target_impl()); + +`downcast` is used to obtain the target type of the downcasting operation registered for a +given instantiation in a base type. `detail::downcast_target_impl` checks if a downcasting +target is registered for the specific base class. If yes, it returns the registered type, +otherwise it works like a regular identity type trait returning a provided base class:: + + namespace detail { + + template + concept has_downcast = requires { + downcast_guide(std::declval>()); + }; + + template + constexpr auto downcast_target_impl() + { + if constexpr(has_downcast) + return decltype(downcast_guide(std::declval>()))(); + else + return T(); + } + + } diff --git a/docs/examples.rst b/docs/examples.rst index b42f5819..f9d1b56b 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -8,3 +8,4 @@ Examples examples/avg_speed examples/measurement examples/linear_algebra + examples/box_example diff --git a/docs/examples/box_example.rst b/docs/examples/box_example.rst new file mode 100644 index 00000000..b25d8921 --- /dev/null +++ b/docs/examples/box_example.rst @@ -0,0 +1,7 @@ +box_example +=========== + +.. literalinclude:: ../../example/box_example.cpp + :caption: box_example.cpp + :start-at: #include + :linenos: diff --git a/docs/introduction.rst b/docs/introduction.rst index 74307c10..16acde27 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -45,3 +45,27 @@ Approach 5. No external dependencies 6. Possibility to be standardized as a freestanding part of the C++ Standard Library + + +With the User's Experience in Mind +---------------------------------- + +Most of the important design decisions in the library are dictated by the requirement of +providing the best user experience as possible. Other C++ physical units libraries are +"famous" for their huge error messages (one line of the error log often do not fit on one +slide). The ultimate goal of **mp-units** is to improve this and make compile-time errors +and debugging as easy and user-friendly as possible. + +To achieve this goal several techniques are applied: + +- usage of C++20 concepts, +- using strong types for framework entities (instead of type aliases), +- limiting the number of template arguments to the bare minimum, +- :ref:`The Downcasting Facility`. + +.. important:: + + In many generic C++ libraries compile-time errors do not happen often. It is hard to + break ``std::string`` or ``std::vector`` in a way it won't compile with a huge error + log. Physical Units libraries are different. **Generation of compile-time errors + is the main reason to create such a library.** diff --git a/docs/use_cases.rst b/docs/use_cases.rst index b3be890d..0f158e94 100644 --- a/docs/use_cases.rst +++ b/docs/use_cases.rst @@ -9,9 +9,9 @@ Use Cases using namespace units::physical; .. toctree:: - :maxdepth: 2 + :maxdepth: 1 - use_cases/unknown_units_and_dimensions + use_cases/unknown_dimensions use_cases/legacy_interfaces use_cases/custom_representation_types use_cases/linear_algebra diff --git a/docs/use_cases/extensions.rst b/docs/use_cases/extensions.rst index 0c8f759c..1f00456a 100644 --- a/docs/use_cases/extensions.rst +++ b/docs/use_cases/extensions.rst @@ -3,17 +3,203 @@ Extending the Library ===================== +The library was designed with a simple extensibility in mind. It is easy to add new units, +dimensions, and prefixes. The systems of units are not closed (classes) but open (namespaces) +and can be easily extended, or its content can be partially/fully imported to other systems. + + Custom Units ------------ +It might happen that the user would like to use a unit that is not predefined by the library +or is predefined but the user would like to name it differently, assign a different symbol +to existing unit, or make it a base unit for prefixes. + + +Defining a New Unit +^^^^^^^^^^^^^^^^^^^ + +My working desk is of ``180 cm x 60 cm`` which gives an area of ``0.3 m²``. I would like to +make it a unit of area for my project:: + + struct desk : named_scaled_unit, si::square_metre> {}; + +With the above I can define a quantity with the area of ``2 desks``:: + + auto d1 = si::area(2); + +In case I feel it is too verbose to type the above every time I can easily create a custom +alias or an :abbr:`UDL (User Defined Literal)`:: + + // alias with fixed integral representation + using desks = si::area; + + // UDLs + constexpr auto operator"" _d(unsigned long long l) { return si::area(l); } + constexpr auto operator"" _d(long double l) { return si::area(l); } + +Right now I am fully set up for my project and can start my work of tracking the area taken +by my desks:: + + auto d1 = si::area(2); + auto d2 = desks(3); + auto d3 = 1_d; + auto sum = d1 + d2 + d3; + std::cout << "Area: " << sum << '\n'; // prints 'Area: 6 desk' + +In case I would like to check how much area ``6 desks`` take in SI units:: + + auto sum_si = quantity_cast(sum); + std::cout << "Area (SI): " << sum_si << '\n'; // prints 'Area (SI): 1.8 m²' + + +Enabling a Unit for Prefixing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In case I decide it is reasonable to express my desks with SI prefixes the only thing I have +to change in the above code is to replace `no_prefix` with `si_prefix`:: + + struct desk : named_scaled_unit, si::square_metre> {}; + +Now I can define a new unit named ``kilodesk``:: + + struct kilodesk : prefixed_unit {}; + static_assert(3_d * 1000 == si::area(3)); + +But maybe SI prefixes are not good for me. Maybe I always pack ``6`` desks into one package +for shipment and ``40`` such packages fit into my lorry. To express this with prefixes a new +prefix family and prefixes are needed:: + + struct shipping_prefix : prefix_family {}; + + struct package : prefix> {}; + struct lorry : prefix> {}; + +Now we can use it for our unit:: + + struct desk : named_scaled_unit, si::square_metre> {}; + struct packagedesk : prefixed_unit {}; + struct lorrydesk : prefixed_unit {}; + +With the above:: + + static_assert(6_d == si::area(1)); + static_assert(240_d == si::area(1)); + std::cout << "Area: " << quantity_cast(sum) << '\n'; // prints 'Area: 1 pkgdesk' + +It is important to notice that with the definition of a custom prefix I did not loose SI +units compatibility. If I want to calculate how much area I can cover with desks delivered +by ``3 lorries`` I can do the following:: + + auto area = quantity_cast(si::area(3)); + std::cout << "Area: " << area << '\n'; // prints 'Area: 216 m²' + + Custom Dimensions ----------------- -Custom Base Dimensions -^^^^^^^^^^^^^^^^^^^^^^ +There are cases were a custom unit is not enough and the user would like to define custom +dimensions. The most common case is to define a new derived dimension from other dimensions +already predefined in various systems. But in **mp-units** library it is also really easy to +define a new base dimension for a custom units system. Custom Derived Dimensions ^^^^^^^^^^^^^^^^^^^^^^^^^ +In case I want to track how many desks I can produce over time or what is the consumption +rate of wood during production I need to define a new derived dimension together with its +coherent unit:: + + // coherent unit must apply to the system rules (in this case SI) + struct square_metre_per_second : unit {}; + + // new derived dimensions + struct dim_desk_rate : derived_dimension, exp> {}; + + // our unit of interest for a new derived dimension + struct desk_per_hour : deduced_unit {}; + + // a quantity of our dimension + template + using desk_rate = quantity; + + // a concept matching the above quantity + template + concept DeskRate = QuantityOf; + +With the above we can now check what is the production rate:: + + DeskRate auto rate = quantity_cast(3._d / 20q_min); + std::cout << "Desk rate: " << rate << '\n'; // prints 'Desk rate: 9 desk/h' + +and how much wood is being consumed over a unit of time:: + + auto wood_rate = quantity_cast(rate); + std::cout << "Wood rate: " << wood_rate << '\n'; // prints 'Wood rate: 0.00075 m²/s' + + +Custom Base Dimensions +^^^^^^^^^^^^^^^^^^^^^^ + +In case I want to monitor what is the average number of people sitting by one desk in +a customer's office I would need a unit called ``person_per_desk`` of a new derived +dimension. However, our library does not know what a ``person`` is. For this I need to +define a new base dimension, its units, quantity helper, concept, and UDLs:: + + struct person : named_unit {}; + struct dim_people : base_dimension<"people", person> {}; + + template + using people = quantity; + + template + concept People = QuantityOf; + + constexpr auto operator"" _p(unsigned long long l) { return people(l); } + constexpr auto operator"" _p(long double l) { return people(l); } + + +With the above we can now define a new derived dimension:: + + struct person_per_square_metre : unit {}; + struct dim_occupancy_rate : derived_dimension, exp> {}; + + struct person_per_desk : deduced_unit {}; + + template + using occupancy_rate = quantity; + + template + concept OccupancyRate = QuantityOf; + +Now we can play with our new feature:: + + People auto employees = 1450._p; + auto office_desks = 967_d; + OccupancyRate auto occupancy = employees / office_desks; + + std::cout << "Occupancy: " << occupancy << '\n'; // prints 'Occupancy: 1.49948 person/desk' + + Custom Systems -------------- + +Being able to extend predefined systems is a mandatory feature of any physical +units library. Fortunately, for **mp-units** there is nothing special to do here. + +A system is defined in terms of its base dimensions. If you are using only SI +base dimensions then you are in the boundaries of the SI system. If you are +adding new base dimensions, like we did in the `Custom Base Dimensions`_ +chapter, you are defining a new system. + +In **mp-units** library a custom system can either be constructed from +unique/new custom base dimensions or reuse dimensions of other systems. This +allows extending, mixing, reuse, and interoperation between different systems. + + +.. seealso:: + + More information on extending the library can be found in the + :ref:`Using Custom Representation Types` chapter. diff --git a/docs/use_cases/unknown_dimensions.rst b/docs/use_cases/unknown_dimensions.rst new file mode 100644 index 00000000..6d990572 --- /dev/null +++ b/docs/use_cases/unknown_dimensions.rst @@ -0,0 +1,109 @@ +.. namespace:: units + +Working with Unknown Dimensions and Their Units +=============================================== + +From time to time the user of this library will face an `unknown_dimension` and +`unknown_coherent_unit` types. This chapters describes their purpose and usage in +detail. + +What is an unknown dimension? +----------------------------- + +As we learned in the :ref:`Dimensions` chapter, in most cases the result of multiplying +or dividing two quantities of specific dimensions is a quantity of yet another dimension. + +If such a resulting dimension is predefined by the user (and a proper header file with its +definition is included in the current translation unit) :ref:`The Downcasting Facility` +will determine its type. The same applies to the resulting unit. For example: + +.. code-block:: + :emphasize-lines: 3,7-9 + + #include + #include + #include + + using namespace units::physical::si; + + constexpr auto result = 144q_km / 2q_h; + static_assert(std::is_same_v); + static_assert(std::is_same_v); + +However, if the resulting dimension is not predefined by the user the library framework +will create an instance of an `unknown_dimension`. The coherent unit of such an unknown +dimension is an `unknown_coherent_unit`. Let's see what happens with our example when +we forget to include a header file with the resulting dimension definition: + +.. code-block:: + :emphasize-lines: 3,9,11 + + #include + #include + // #include + + using namespace units::physical::si; + + constexpr auto result = 144q_km / 2q_h; + static_assert(std::is_same_v, exp>>); + static_assert(std::is_same_v, unknown_coherent_unit>>); + + +Operations On Unknown Dimensions And Their Units +------------------------------------------------ + +For some cases we can eliminate the need to predefine a specific dimension and just use +the `unknown_dimension` instead. Let's play with the previous example a bit:: + + static_assert(result.count() == 72); + +As we can see the value stored in this quantity can be easily obtained and contains a +correct result. However, if we try to print its value to the text output we will get:: + + std::cout << "Speed: " << result << '\n'; // prints 'Speed: 72 [1/36 × 10¹] m/s' + +The output from above program should not be a surprise. It is an unknown dimensions with +a scaled unknown coherent unit. The library can't know what is the symbol of such unit +so it does its best and prints the unit in terms of units of base dimensions that formed +this particular unknown derived dimension. + +In case we would like to print the result in terms of base units we can simply do the +following:: + + auto s = quantity_cast(result); + std::cout << "Speed: " << s << '\n'; // prints 'Speed: 20 m/s' + +.. seealso:: + + Another good example of unknown dimension usage can be found in the + :ref:`box_example`:: + + std::cout << "float rise rate = " << box.fill_level(measured_mass) / fill_time << '\n'; + + +Temporary Results +----------------- + +In many cases there is nothing inherently wrong with having unknown dimensions and units +in your program. A typical example here are temporary results of a long calculation: + +.. code-block:: + :emphasize-lines: 5,7 + + auto some_long_calculation(Length auto d, Time auto t) + { + Speed auto s1 = avg_speed(d, t); + + auto temp1 = s1 * 200q_km; // intermediate unknown dimension + + Speed auto s2 = temp1 / 50q_km; // back to known dimensions again + Length auto d2 = s2 * 4q_h; + + // ... + } + +If a programmer wants to break the calculation to several lines/variables he/she does not +have to ensure that the intermediate results are of predefined dimensions or just a clear +science fiction :-) The final result will always be correct. diff --git a/docs/use_cases/unknown_units_and_dimensions.rst b/docs/use_cases/unknown_units_and_dimensions.rst deleted file mode 100644 index 8bf3a88b..00000000 --- a/docs/use_cases/unknown_units_and_dimensions.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. namespace:: units - -Working with Unknown Units and Dimensions -========================================= - -- what is an unknown unit? -- what is an unknown dimension? -- temporary result -- casting to the coherent unit