Magnitudes físicas: El problema de la conversión de unidades

Introducción


El desgraciado error de conversión de unidades que condujo en 1999 a la desintegración de la sonda Mars Climate Orbiter de la NASA es un ejemplo bien conocido de la necesidad de garantizar que nuestro software sea fuertemente tipado, así como del peligro de emplear un mismo tipo primitivo básico para representar magnitudes diferentes y/o medidas de una única magnitud expresadas en unidades distintas.


En efecto, imaginemos que un software científico utilizara el tipo double para registrar distancias tanto en el Sistema Internacional (cuya unidad fundamental es el metro) como en el Sistema Anglosajón (por ejemplo, mediante el empleo de millas). Ciertamente, la elección de un tipo coma flotante como double para la representación de nuestras medidas puede parecer completamente lógica a primera vista. El siguiente ejemplo, sin embargo, muestra claramente los riesgos a los que nos enfrentamos. En él, la función distance_calculation espera recibir una distancia expresada en el Sistema Internacional, pero recibe por error un input dist expresado implícitamente en el Sistema Anglosajón:

auto distance_calculation(double d) -> void { // se espera que d esté expresado en metros // para realizar cálculos posteriores... } //... auto dist = 8.21; // dist está dado implícitamente en millas distance_calculation(dist); // oops!

¿Cómo prevenir errores de esta naturaleza? De insistir en utilizar la misma representación (double) para distancias en metros y en millas, esta incidencia sólo podría ser evitada a través de una adecuada documentación de nuestras funciones y un riguroso proceso de validación del software.

Existe una solución óptima, sin embargo, que se apoya en el sistema de tipos de C++ para localizar y corregir incidencias de esta naturaleza en tiempo de compilación. Dedicaremos este post a explicar los aspectos fundamentales de esta técnica. Por supuesto, nuestra implementación tendrá una motivación fundamentalmente pedagógica, recomendándose el uso de bibliotecas profesionales de gestión de unidades (por ejemplo, Boost.Unitshttps://www.boost.org/doc/libs/1_71_0/doc/html/boost_units.html).

Clases de cantidades físicas


En primer lugar, es claro que cada unidad de medida (metro, centímetro, milla, kilogramo, segundo, etcétera) debería identificarse a través de un tipo propio. C++ carece en la actualidad de strong typedefs, pero no resulta complicado emularlos mediante el diseño de un wrapper que dote de individualidad a cada unidad de medida que empleemos:

#include <fmt/format.h> // {fmt}lib - Text formatting #include <iostream> #include <ratio> #include <type_traits> template<class Quantity, typename Value, class Convert> struct Physical_quantity { using quantity_type = Quantity; using value_type = Value; using convert_type = Convert; value_type value; explicit Physical_quantity(value_type const& v) : value{v} { }
template<class C> Physical_quantity(Physical_quantity<quantity_type, value_type, C> const& q) { value = convert_type::from_si(C::to_si(q.value)); } template<class C> auto& operator+=(Physical_quantity<quantity_type, value_type, C> const& rhs) { value += convert_type::from_si(C::to_si(rhs.value)); return *this; } // ... }; template<class Q, typename V, class C_lhs, class C_rhs> auto operator+(Physical_quantity<Q, V, C_lhs> lhs, Physical_quantity<Q, V, C_rhs> const& rhs) { return lhs += rhs; }

Aquí, Physical_quantity es la plantilla de clase que emplearemos para almacenar magnitudes físicas (la unión de un valor experimental y una unidad de medida). Distinguimos tres parámetros:
  • Quantity: La etiqueta que diferenciará una magnitud física (longitud, masa, tiempo, etcétera) del resto.
  • Value: El tipo subyacente para la representación numérica de nuestros datos (long double en el caso de distancias).
  • Convert: Una clase que asociaremos a cada unidad de medida. Deberá constar de dos funciones miembro públicas estáticas de signaturas from_si(value_type)->value_type y to_si(value_type)->value_type, que conviertan dicha unidad desde o hacia la correspondiente unidad básica del Sistema Internacional, respectivamente. Así, por ejemplo, en el caso de la milla, la función from_si() retornará el resultado de dividir el valor numérico de la distancia entre 1609.34 (conversión de metro a milla), mientras que la función to_si() devolverá el resultado de multiplicarlo por dicho factor (conversión de milla a metro). Puede encontrarse una discusión más detallada acerca de la necesidad de utilizar estas funciones auxiliares en el siguiente post del blog de programación Fluent{C++}:

https://www.fluentcpp.com/2017/05/26/strong-types-conversions/

La plantilla Physical_quantity contiene una única variable pública de nombre value que almacenará el valor numérico de la magnitud física. El constructor-plantilla (template constructor) es el encargado de realizar las conversiones de unidades mediante el uso de las funciones estáticas from_si() y to_si() de las clases Convert. Asimismo, hemos sobrecargado los operadores operator+= y operator+ con el fin de permitir, al menos, la suma de medidas de una misma magnitud física.

Llegados a este punto, podemos definir la magnitud física de distancia Length como una especialización parcial de la plantilla Physical_quantity en la que el primer argumento viene dado por una estructura vacía struct Length_tag que juega el papel de mera etiqueta y el segundo argumento (el tipo de representación subyacente) viene dado por long double. La unidad básica de distancia del Sistema Internacional (el metro) y sus unidades derivadas (como el centímetro o el kilómetro, múltiplos del metro) se definen proporcionando clases de conversión Convert apropiadas para cada una de ellas. Por conveniencia, introduciremos también un sufijo personalizado (user-defined literal) para cada unidad:

template<class Convert> using Length = Physical_quantity<struct Length_tag, long double, Convert>; template<class Ratio> struct Multiple_convert { static auto to_si(long double val) { return Ratio::num * val / Ratio::den; } static auto from_si(long double val) { return Ratio::den * val / Ratio::num; } }; using Meter = Length<Multiple_convert<std::ratio<1>>>; // SI base unit using Centimeter = Length<Multiple_convert<std::centi>>; using Kilometer = Length<Multiple_convert<std::kilo>>; auto operator""_m(long double d) { return Meter{d}; } auto operator""_cm(long double d) { return Centimeter{d}; } auto operator""_km(long double d) { return Kilometer{d}; }

La milla, que pertenece a un sistema de unidades distinto, requiere una estructura de conversión especializada:

struct Miles_convert { static auto to_si(long double d) { return 1'609.34 * d; } static auto from_si(long double d) { return d / 1'609.34; } }; using Mile = Length<Miles_convert>; auto operator""_mi(long double d) { return Mile{d}; }

De utilizar la conocida biblioteca {fmt}lib (https://fmt.dev/latest/index.html) para dar formato a los strings, puede resultar útil personalizar la presentación de las magnitudes físicas:

namespace fmt { template<typename Q, typename V, typename C> struct formatter<Physical_quantity<Q, V, C>> { template<typename ParseContext> constexpr auto parse(ParseContext& ctx) { return ctx.begin(); } template<typename FormatContext> auto format(Physical_quantity<Q, V, C> const& q, FormatContext& ctx) { return format_to(ctx.out(), "{:.2f}", q.value); } }; } // fmt NAMESPACE

Ejemplo de uso


Podemos ya modificar nuestro ejemplo original y demostrar la seguridad de nuestro código frente a conversiones silenciosas de unidades:

auto distance_calculation(Meter dist) -> void { dist += 7.5_cm + 3.0_km; // (D) fmt::print("Distance: {} meters\n", dist); // (E) } auto main() -> int { auto d = 8.21_mi; // (A) fmt::print("Distance: {} miles\n", d); // (B) distance_calculation(d); // (C) }

En la línea (A) inicializamos una variable de tipo Mile de valor 8.21 millas. Así lo confirma su imprensión en pantalla en (B). Esta variable es utilizada como input por la función distance_calculation en (C), la cual espera sin embargo distancias de tipo Meter. Ello no supone ningún inconveniente: el constructor de conversión de la plantilla Physical_quantity se encarga de inicializar una variable Meter a través de la conversión de unidades adecuada. Es más, a dicha distancia se le suman en (D) longitudes expresadas en centímetros y kilómetros. Los operadores de suma sobrecargados se encargan de que estas operaciones se realicen de forma apropiada, de tal manera que el valor final de la variable impreso en (E) (16212.76 metros) es precisamente el esperado. Notemos también que el empleo de tipos distintos para las unidades físicas provocará errores de compilación siempre que no existan conversiones entre ellas (por ejemplo, al pasar una masa como input a una función que espere distancias). Todo ello demuestra la enorme utilidad que una biblioteca de gestión de unidades puede tener en el ámbito científico-técnico.

No hay comentarios:

Publicar un comentario