Sobre este artículo
Tiempo estimado de lectura: 20 minutos
Nivel: Intermedio
Última actualización: 05 de mayo, 2026
Nivel: Intermedio
Última actualización: 05 de mayo, 2026
1. Introducción y terminología
|
| Imagen generada con inteligencia artificial. Uso con fines divulgativos |
En entornos científicos y tecnológicos, el manejo implícito de unidades físicas es una fuente constante de errores que puede derivar en fallos críticos y grandes costes operativos. En C++, adoptar un sistema de tipado fuerte para magnitudes permite que el compilador actúe como un motor de verificación, detectando inconsistencias dimensionales y resolviendo la lógica de conversión de unidades antes de que tenga lugar la ejecución. Este enfoque logra la seguridad sin penalización en el rendimiento (zero-overhead abstraction [1]), generando un código binario tan eficiente como si se hubiesen empleado tipos numéricos fundamentales (típicamente double), pero eliminando peligrosos errores de magnitudes en tiempo de ejecución.
En este artículo —una revisión de una publicación anterior sobre análisis dimensional en C++— presentaremos algunas de las principales funcionalidades de la biblioteca de magnitudes y unidades físicas mp-units v2 en el ámbito científico y de la industria [2]. Basándose en el estándar ISO/IEC 80000 (Sistema Internacional de Magnitudes, ISQ), la biblioteca cubre desde el análisis dimensional y la conversión de unidades hasta la validación de operaciones entre magnitudes, existiendo una propuesta de estandarización para C++29 [3].
Como parte de la terminología fundamental relativa a magnitudes, y con el fin de entender mejor el funcionamiento de la biblioteca, distinguiremos cuatro conceptos principales:
- Dimensión: define la dependencia de una magnitud respecto a las magnitudes de base de un sistema de magnitudes particular. Se expresa formalmente como un producto de potencias de las magnitudes de base, omitiendo cualquier factor numérico. En el sistema ISQ, tales magnitudes de base son: longitud (L), masa (M), tiempo (T), corriente eléctrica (I), temperatura termodinámica (Θ), cantidad de sustancia (N) e intensidad luminosa (J). Por ejemplo, las dimensiones de la aceleración son, en este sistema, L·T -2.
- Naturaleza de una magnitud (en inglés, kind of quantity): aspecto común a magnitudes que son mutuamente comparables (como el radio y el ancho, ambos de naturaleza "longitud"). No pueden sumarse ni restarse dos o más cantidades a menos que pertenezcan a la misma categoría de magnitudes mutuamente comparables.
- Referencia: es el estándar de comparación que dota de significado al valor numérico de una magnitud. Aunque habitualmente es una unidad de medida (como m·s -2 para una aceleración en el Sistema Internacional de Unidades, SI), también puede ser un procedimiento o un material de referencia.
- Cantidad: instancia concreta que combina un valor numérico (escalar) con una referencia.
A partir de las definiciones anteriores, la biblioteca mp-units introduce las siguientes plantillas de clase en el espacio de nombres mp_units:
1. Para representar intervalos o diferencias de una magnitud mediante una cantidad específica:
template<Reference auto R,
RepresentationOf<get_quantity_spec(R)> Rep = double>
class quantity;2. Para representar cantidades medidas desde un origen específico (puntos de medida):
template<Reference auto R,
PointOriginFor<get_quantity_spec(R)> auto PO = default_point_origin(R),
RepresentationOf<get_quantity_spec(R)> Rep = double>
class quantity_point;
Como puede comprobarse, la representación numérica subyacente en ambos casos es, por defecto, double.
En este artículo, haremos uso de intervalos quantity exclusivamente. Una cualquiera de sus instancias puede crearse fácilmente sin más que multiplicar/dividir un valor numérico y una referencia. Por ejemplo, auto q1 = 2.5*m; crea una instancia de tipo quantity<isq::length[m]>. El tipo de la magnitud puede también especificarse explícitamente, como en auto q2 = quantity<isq::distance[cm]>{8.9} o, equivalentemente, auto q2 = 8.9*isq::distance[cm];.
Aprovechando las capacidades de C++20 y estándares posteriores (específicamente, el uso de concepts), la biblioteca mp-units proporciona un marco de seguridad estructurado en seis niveles [4]:
- Dimensional (dimension safety): impide la interacción entre magnitudes con dimensiones físicas incompatibles.
- De unidades (unit safety): previene errores entre unidades y elimina la necesidad de factores de conversión manuales.
- De representación (representation safety): protege contra desbordamientos (overflows) y pérdida de precisión aritmética.
- De naturaleza de la magnitud (quantity kind safety): deshabilita las operaciones aritméticas entre cantidades que, aun compartiendo dimensiones, poseen naturalezas físicas distintas y, por tanto, no son comparables.
- De magnitudes (quantity safety): valida que las ecuaciones y relaciones entre magnitudes sean físicamente correctas.
- De espacio matemático (mathematical space safety): establece una distinción semántica y operativa entre puntos de medida y deltas.
En este artículo, haremos uso de los niveles de seguridad de 1 a 5. El nivel 6 puede alcanzarse al emplear, donde corresponda, instancias quantity_point.
NOTA 1: NIVEL 5 DE SEGURIDAD DE MAGNITUDES
La seguridad de magnitudes (quantity safety) permite distinguir cantidades de idénticas dimensiones, como por ejemplo especializaciones de longitudes (length) tales como alturas (height), anchuras (width) o radios (radius).
En efecto, en la biblioteca, tanto isq::height como isq::distance son especializaciones de isq::length. Aunque comparten la misma dimensión (L) y pueden usar las mismas unidades (m en el SI), mp-units permite tratarlas como tipos distintos a fin de evitar errores semánticos. Así, la siguiente función, que espera específicamente una altura como argumento, no admitirá distancias:
using namespace mp_units:si::unit_symbols;
using namespace mp_units;
auto print_height(quantity<isq::height[m]> h) -> void
{
fmt::println("{::N[.2f]U[dn]}", h);
}
auto d = 2.5*isq::distance[m];
print_height(d); // fallo de compilación
El código anterior conducirá a un error de compilación equivalente a:
could not convert 'd' from
'quantity<mp_units::reference<mp_units::isq::distance, mp_units::si::metre>(),[...]>'
to 'quantity<mp_units::reference<mp_units::isq::height, mp_units::si::metre>(),[...]>'
pudiendo hacerse funcionar mediante una conversión explícita:
print_height(quantity_cast<isq::height>(d)); // OK
2. Un ejemplo histórico: el Mars Climate Orbiter
![]() |
| Recreación artística generada con inteligencia artificial basada en descripciones de la misión Mars Climate Orbiter de la NASA (1999). No corresponde a una imagen real. Uso con fines divulgativos |
Este fallo provocó un error acumulado en la trayectoria, haciendo que la nave pasase sobre la superficie de Marte a una altitud de sólo 57 km, cuando se esperaba que lo hiciera a 140–150 km, quedando probablemente desintegrada por la fricción con la atmósfera [5].
Este error podría haberse evitado empleando bibliotecas de tratamiento de magnitudes físicas como la que nos ocupa. Así, el código inseguro
auto apply_thrust_impulse(double impulse_newton_seconds) -> void // espera impulsos en N·s
{
fmt::println("Processing impulse = {} N·s", impulse_newton_seconds);
}
// ...
// valor calculado en el sistema de tierra (implícitamente en lbf·s);
auto ground_impulse_pound_force_seconds = double{150.0};
// error fatal: paso del valor directamente sin convertir a N·s (1 lbf·s ≈ 4.44822 N·s):
apply_thrust_impulse(ground_impulse_pound_force_seconds); // imprime: "Processing impulse = 150 N·s"
podría ser corregido en la forma:
#include <mp-units/systems/si.h> // Sistema Internacional (SI)
#include <mp-units/systems/yard_pound.h> // sistema yarda-libra
using namespace mp_units::si::unit_symbols;
using namespace mp_units::yard_pound::unit_symbols;
using namespace mp_units;
auto apply_thrust_impulse(quantity<isq::impulse[N*s]> impulse) -> void
{
fmt::println("Processing impulse = {::N[.2f]U[dn]}", impulse);
}
// ...
// el software de tierra genera el valor explícitamente en libras-fuerza segundo (lbf·s):
auto ground_impulse = 150.0*lbf*s;
// OK, se realiza la conversión de unidades:
apply_thrust_impulse(ground_impulse); // imprime: "Processing impulse = 667.23 N⋅s"
3. Experimento: medición de la aceleración de la gravedad mediante tiros parabólicos
Consideraremos un experimento de laboratorio en el que un proyectil esférico es sometido a múltiples lanzamientos parabólicos bajo la acción de la gravedad g. Todos los tiros se producen a una misma altura h sobre la superficie horizontal de impacto, con una velocidad inicial que se determina a través de un dispositivo digital con barrera de luz. Sea d la pequeña distancia de separación entre el punto de lanzamiento del proyectil y el centro de la barrera de luz y vexp la velocidad medida por dicho dispositivo. Sea φ el ángulo de salida con respecto a la horizontal y r el alcance horizontal (punto de impacto) con respecto a la vertical de lanzamiento (véase la figura derecha).
Corrigiendo la velocidad inicial del tiro para tomar en consideración la distancia d, y despreciando la resistencia con el aire, se cumple la relación:
Supongamos que se ha procedido al lanzamiento del proyectil para una serie de ángulos distintos φ (medidos en grados sexagesimales), anotándose para cada tiro la velocidad vexp (en millas por hora) y el alcance r (en centímetros). Las ternas (φ,vexp,r) se encuentran registradas como líneas de un fichero CSV data/parabolic_throwing_data.csv :
ángulo (deg), velocidad (mi/h), alcance (cm) 0, 5.68, 61.5 5, 5.66, 64.5 10, 5.64, 72.0 15, 5.61, 77.3 20, 5.59, 82.0 25, 5.55, 85.5 30, 5.50, 87.5 35, 5.50, 89.3 40, 5.53, 89.0 45, 5.48, 86.0 50, 5.53, 82.5 55, 5.48, 77.0 60, 5.50, 70.0 65, 5.50, 60.5 70, 5.53, 51.5 75, 5.50, 41.0 0, 7.99, 87.5 5, 7.99, 99.5 10, 7.99, 113.0 15, 7.94, 124.5 20, 7.85, 135.5 25, 7.83, 145.0 30, 7.90, 152.5 35, 7.81, 157.0 40, 7.85, 158.5 45, 7.70, 156.5 50, 7.83, 152.0 55, 7.83, 141.5 60, 7.83, 128.5 65, 7.74, 111.5 70, 7.85, 95.5 75, 7.78, 72.0
Los parámetros de configuración del experimento (distancia d y altura h, ambas en centímetros) se encuentran recogidos en un segundo CSV data/experimental_setup.csv:
d (cm), h (cm) 3.3, 29.8
Implementaremos un programa que calcule la aceleración de la gravedad en el laboratorio (g) garantizando la corrección dimensional y de unidades en todo momento. El cálculo se basará en el algoritmo iterativo de Levenberg-Marquardt para regresiones no-lineales [6], disponible en la biblioteca Dlib [7]. Asimismo, utilizaremos la biblioteca Matplot++ [8] para la generación de gráficos científicos.
4. Implementación: uso combinado de las bibliotecas mp-units, Dlib y Matplot++
Emplearemos la implementación experimental del estándar C++26 proporcionada por el compilador GCC 16.1.
Procederemos, en primer lugar, a incluir las cabeceras a emplear en el programa y a definir varios alias de espacios de nombres. Para obtener el código completo de esta sección, bastará concatenar los bloques de código en el mismo orden que se proporcionan:
Procederemos, en primer lugar, a incluir las cabeceras a emplear en el programa y a definir varios alias de espacios de nombres. Para obtener el código completo de esta sección, bastará concatenar los bloques de código en el mismo orden que se proporcionan:
#include <algorithm>
#include <exception>
#include <span>
#include "quantity_csv.hpp"
#include <mp-units/systems/isq_angle.h>
#include <mp-units/systems/si.h>
#include <mp-units/systems/yard_pound.h>
#include <dlib/optimization.h>
#include <fmt/core.h>
#include <matplot/matplot.h>
namespace stdr = std::ranges;
namespace stdv = std::views;
namespace mpu = mp_units;
using namespace mpu::si::unit_symbols; // símbolos de unidades del SI
using namespace mpu::yard_pound::unit_symbols; // símbolos de unidades del sistema yarda-libra
La siguiente función genérica hrange calcula el alcance teórico de un tiro parabólico para un ángulo de salida φ y una velocidad inicial vexp dadas, conocidas las constantes de configuración d y h y la aceleración g. Se adopta aquí un diseño genérico y con seguridad de unidades:
[[nodiscard]] auto hrange(
mpu::QuantityOf<mpu::isq::distance> auto d, // distancia recorrida en barrera de luz
mpu::QuantityOf<mpu::isq::height> auto h, // altura inicial del tiro parabólico
mpu::QuantityOf<mpu::isq_angle::angular_measure> auto phi, // ángulo de salida φ
mpu::QuantityOf<mpu::isq::speed> auto v_exp, // velocidad medida por barrera de luz
mpu::QuantityOf<mpu::isq::acceleration> auto g // aceleración de la gravedad
) noexcept
-> mpu::QuantityOf<mpu::isq::length> auto // alcance máximo horizontal teórico
{
using namespace mpu;
QuantityOf<dimensionless> auto const c = angular::cos(phi),
s = angular::sin(phi);
QuantityOf<pow<2>(isq::speed)> auto const v0_sq = v_exp*v_exp + 2*g*d*s;
QuantityOf<dimensionless> auto const radicand = s*s + 2*h*g/v0_sq;
return c*(s + sqrt(radicand))*v0_sq/g;
}
Al emplear el concepto mp_units::QuantityOf:
- Garantizamos en tiempo de compilación que cada argumento satisfaga la especificación de magnitud requerida. Ello impide, por ejemplo, que pasemos accidentalmente un "tiempo" o una "masa" donde se espera una "aceleración", al detectarse en tales casos dimensiones incompatibles.
- Derivamos automáticamente las unidades intermedias —como la velocidad al cuadrado—, eliminando el riesgo de trabajar indebidamente con factores de conversión de forma manual.
- Aseguramos que la expresión final devuelva estrictamente una magnitud de longitud.
NOTA 2: VERIFICACIÓN DE ECUACIONES EN COMPILACIÓN
La función anterior hrange calcula el alcance máximo horizontal teórico del tiro parabólico, tal y como quedó expresado en la ecuación proporcionada en la sección 3. Cualquier error en la transcripción de la fórmula producirá un error dimensional en tiempo de compilación. A modo de ejemplo, la sentencia de retorno siguiente (obsérvese la ausencia de división por g)
return c*(s + sqrt(radicand))*v0_sq /* /g */;
conducirá a un error de compilación equivalente a:
constraints not satisfied
the expression
'mp_units::implicitly_convertible(T{}, QS)
[with T = mp_units::derived_quantity_spec<
mp_units::power<mp_units::isq::acceleration, 1, 2>,
mp_units::power<mp_units::isq::height, 1, 2>, mp_units::isq::speed>;
QS = mp_units::isq::length{}]'
evaluated to 'false'
en el que se pone de manifiesto claramente que las dimensiones del valor retornado ((L·T -2)1/2·L1/2·(L·T -1) = L2·T -2) no son coincidentes con la dimensión de retorno esperada L (la propia de una longitud).
A continuación, introduciremos una serie de alias convenientes para las magnitudes con las que trabajaremos (con unidades explícitas), así como agregados de datos para las cantidades almacenadas en los ficheros CSV. El resto del código se adaptaría de forma transparente ante cualquier futura modificación en las unidades de las magnitudes:
inline constexpr struct horizontal_range final : mpu::quantity_spec<mpu::isq::length>
{ } horizontal_range; // nuevo tipo de "longitud" para alcances horizontales del proyectil
using acceleration_type = mpu::quantity<mpu::isq::acceleration[m/s2]>; // para g
using angle_type = mpu::quantity<mpu::isq_angle::angular_measure[
mpu::angular::unit_symbols::deg]>; // para φ
using distance_type = mpu::quantity<mpu::isq::distance[cm]>; // para d
using height_type = mpu::quantity<mpu::isq::height[cm]>; // para h
using hrange_type = mpu::quantity<horizontal_range[cm]>; // para r
using speed_type = mpu::quantity<mpu::isq::speed[mi/h]>; // para vₑₓₚ
struct shot_data_type { // CSV #1: ternas (φ,vₑₓₚ,r)
angle_type phi;
speed_type v_exp;
hrange_type r;
};
struct experimental_setup_type { // CSV #2: pareja (d,h)
distance_type d;
height_type h;
};
Consideremos un contenedor std::vector<shot_data_type> conteniendo las ternas experimentales (φ, vexp, r) (dotadas de las unidades correspondientes), así como un objeto experimental_setup_type con los valores de configuración d y h (también con unidades incorporadas). La siguiente función lm_analysis empleará el método de Levenberg-Marquardt dlib::solve_least_squares_lm (proporcionado en la biblioteca Dlib) para calcular el valor óptimo de la aceleración de la gravedad g que minimiza el error estándar de los residuos (RSE):
struct Analysis_results {
acceleration_type g; // valor óptimo de aceleración de la gravedad
acceleration_type sigma_g; // desviación estándar
hrange_type rse; // error estándar de los residuos
};
[[nodiscard]] auto lm_analysis(
std::vector<shot_data_type> const& data,
experimental_setup_type const& setup_values
) -> Analysis_results
{
if (data.size() < 2) {
throw std::runtime_error{"el archivo CSV debe contener al menos dos ternas (φ,vexp,r)"};
}
static constexpr auto acc_unit = m/s2; // unidad de medida de aceleración en el SI
using parameter_type = dlib::matrix<acceleration_type::rep, 1, 1>; // un único parámetro a ajustar: g
auto const& [d, h] = setup_values;
auto residual = [&](shot_data_type const& info, parameter_type const& param) -> double {
auto const& [phi, v_exp, r] = info;
auto const g = acceleration_type{param(0)*acc_unit};
return (hrange(d, h, phi, v_exp, g) - r).numerical_value_in(m);
};
// control manual de unidades ----------------------------------------------------------------
auto param = parameter_type{(10.*acc_unit).numerical_value_in(acc_unit)}; // valor de partida
auto deriv = dlib::derivative(residual);
auto const rss = 2.0*dlib::solve_least_squares_lm( // implícitamente en m2
dlib::objective_delta_stop_strategy{},
residual,
deriv,
data,
param
);
auto const sum_jacobian_sq = stdr::fold_left( // implícitamente en s4
data | stdv::transform([&](auto const& info) {
auto const j = deriv(info, param)(0);
return j*j;
}),
0.0,
std::plus{}
);
auto const rse = std::sqrt(rss / (data.size() - 1)); // implícitamente en m
// -------------------------------------------------------------------------------------------
return Analysis_results{
.g = param(0)*acc_unit,
.sigma_g = (rse / std::sqrt(sum_jacobian_sq))*acc_unit, // OK: m/sqrt(s4) = m/s2
.rse = hrange_type{rse*m}
};
}
El modelo distingue a g como único parámetro. La expresión lambda residual es la encargada de calcular los residuos para el problema de mínimos cuadrados. Ésta toma una terna experimental (φ,vexp,r) contenida en una tupla shot_data_type y compara el valor teórico hrange evaluado en (d,h,φ,vexp,g) con el correspondiente alcance horizontal experimental r. Ante la relativamente complicada expresión adoptada por la derivada parcial del residuo respecto a g, aproximamos numéricamente su valor a (residual(info,g+eps)-residual(info,g-eps))/(2*eps) —siendo eps = 1.e-7— a través del método dlib::derivative.
NOTA 2: BIBLIOTECAS SIN SEGURIDAD DE UNIDADES
Observemos que la mayoría de las bibliotecas de optimización numérica, como Dlib, están diseñadas para trabajar con tipos fundamentales coma flotante (típicamente double) que no informan del significado físico de las variables.
Al emplear el algoritmo de Levenberg-Marquardt, nuestra función lm_analysis debe reducir las magnitudes a números puros (extrayendo su valor numérico en una unidad de referencia, como m/s²) para que Dlib pueda realizar operaciones de cálculo, como derivadas y matrices de covarianza.
mp-units facilita esta transición mediante métodos explícitos como .numerical_value_in(unit). Una vez que DLib encuentra el óptimo numérico para g, los resultados son reencapsulados inmediatamente en tipos de mp-units para recuperar la seguridad dimensional.
Una vez obtenido el valor óptimo de g, nos serviremos de la biblioteca Matplot++ para visualizar la superficie de mejor ajuste r = hrange(d,h,φ,vexp,g):
auto plot_graphics(
std::span<shot_data_type const> data,
experimental_setup_type const& setup_values,
acceleration_type const& g
) -> void
{
auto numerical_values_of = [data]<mp_units::Quantity Q>(Q shot_data_type::* projection)
-> std::vector<typename Q::rep>
{
return data
| stdv::transform(projection)
| stdv::transform([](Q const& q) { return q.numerical_value_in(Q::unit); })
| stdr::to<std::vector>();
};
// ángulos (p), velocidades (s) y alcances (r) numéricos (sin unidades):
auto const p = numerical_values_of(&shot_data_type::phi);
auto const s = numerical_values_of(&shot_data_type::v_exp);
auto const r = numerical_values_of(&shot_data_type::r);
auto const [pmin, pmax] = stdr::minmax_element(p); // iteradores a valores mín y máx en p
auto const [smin, smax] = stdr::minmax_element(s); // ídem para s
matplot::gcf()->quiet_mode(matplot::on);
matplot::xlabel(fmt::format("φ ({})", angle_type::unit));
matplot::ylabel(fmt::format("vₑₓₚ ({})", speed_type::unit));
matplot::zlabel(fmt::format("r ({})", hrange_type::unit));
auto const& [d, h] = setup_values;
// superficie r vs (p, s) para el valor óptimo de g, superpuesta a los puntos experimentales:
auto f = [&](double p, double s) -> double {
return hrange(d, h, angle_type{p*angle_type::unit}, speed_type{s*speed_type::unit}, g)
.numerical_value_in(hrange_type::unit);
};
matplot::fsurf(f, {*pmin, *pmax}, {*smin, *smax})->face_alpha(.7);
matplot::colorbar();
matplot::hold(matplot::on);
matplot::stem3(p, s, r, "filled")->color("r").line_width(1.5).marker_face_color("w");
matplot::hold(matplot::off);
matplot::show();
}
Tan sólo resta implementar un parseador load_quantities que mapee valores numéricos almacenados en una línea de un fichero CSV a un agregado de magnitudes físicas T, incorporando las unidades que correspondan. La función recibirá una ruta path al fichero CSV y deducirá el listado de magnitudes a parsear Q1,Q2,...,QN a partir del agregado mediante mecanismos de reflexión. N denota aquí el número de valores por línea en el CSV. Cada tipo Qi debe cumplir el concepto mp_units::Quantity (es decir, debe representar una magnitud física) y ser parseable:
template<aggregate_of_quantities T>
[[nodiscard]] auto load_quantities(
std::filesystem::path const& pth, // dirección del fichero CSV
bool drop_header = true, // ¿existe una cabecera que ignorar?
char delim = ',' // delimitador de valores
) -> std::expected<std::vector<T>, csv_error>;El parseador retornará un objeto std::expected. En caso de éxito, el valor almacenado será un vector de instancias del agregado T, std::vector<T>, con una entrada por cada línea del CSV. En caso de error, obtendremos un objeto csv_error informando de una incidencia en el flujo de datos o una incoherencia de formato. Tanto la lógica del parseador como una función adicional process_csv_error diseñada para la gestión robusta de errores se encapsularán en la cabecera quantity_csv.hpp (véase la implementación proporcionada en la sección 5 de este artículo).
Las distintas funciones implementadas son invocadas desde la función principal main(), imprimiendo en la terminal el valor óptimo alcanzado para g y visualizando la gráfica de mejor ajuste en una ventana emergente:
auto main() -> int
try {
// colección de ternas experimentales (φ [deg], v_exp [mi/h], r [cm]):
auto const data = qcsv::load_quantities<shot_data_type>("data/parabolic_throwing_data.csv");
if (not data) {
fmt::print(stderr, "{}", qcsv::process_csv_error(data.error()));
return EXIT_FAILURE;
}
// valores de longitud de barrera de luz (d) y altura inicial de los tiros parabólicos (h):
auto const setup = qcsv::load_quantities<experimental_setup_type>("data/experimental_setup.csv");
if (not setup) {
fmt::print(stderr, "{}", qcsv::process_csv_error(setup.error()));
return EXIT_FAILURE;
}
auto const& setup_values = (*setup)[0];
// análisis de mínimos cuadrados LM - valor óptimo de g, desviación estándar y RSE:
auto const [g, sigma_g, rse] = lm_analysis(*data, setup_values);
fmt::println("Experimental value: g = {::N[.2f]U[dn]} ± {::N[.2f]U[dn]} (RSE = {::N[.2f]})",
g, sigma_g, rse);
fmt::println(" Expected value: g = {::N[.2f]U[dn]}",
mpu::quantity{1.*mpu::si::standard_gravity}.in(g.unit));
// gráfica de ajuste de mínimos cuadrados correspondientes al valor óptimo de g:
plot_graphics(*data, setup_values, g);
return EXIT_SUCCESS;
}
catch (std::exception const& e) {
fmt::print(stderr, "exception: {}", e.what());
return EXIT_FAILURE;
}
Experimental value: g = 9.75 m⋅s⁻² ± 0.05 m⋅s⁻² (RSE = 2.17 cm)
Expected value: g = 9.81 m⋅s⁻²El valor del RSE coincide aproximadamente con el radio del cráter de impacto que el proyectil realizaba, en el experimento, sobre un camino de arena fina que frenaba su caída.
5. Parseador CSV para magnitudes físicas
Se proporciona a continuación el contenido del fichero de cabecera quantity_csv.hpp (simple CSV parser). Distinguimos las siguientes funciones:
- detail::parse_numerical_values: convierte una línea de texto en una tupla de valores numéricos adimensionales, gestionando errores de formato, valores faltantes o columnas sobrantes a través de un flujo de entrada.
- detail::parse_quantities: actúa como un envoltorio de la función anterior para mapear los valores parseados a los datos miembro de un agregado de magnitudes físicas, asociándoles automáticamente las unidades que correspondan.
- detail::getlines: implementa un generador de corrutinas que lee un flujo de entrada línea a línea de forma perezosa.
- load_quantities: se encarga de la lectura completa del archivo CSV, validando su extensión y procesando cada fila para devolver un vector de instancias del agregado, o bien un error detallado.
- process_csv_error: transforma los códigos de error (tanto de flujo a fichero como de parseo) en mensajes de texto legibles, incluyendo la ubicación exacta del fallo (línea y columna del CSV).
#ifndef QUANTITY_CSV_HPP
#define QUANTITY_CSV_HPP
#include <expected>
#include <filesystem>
#include <format>
#include <fstream>
#include <functional>
#include <generator>
#include <istream>
#include <meta>
#include <spanstream>
#include <ranges>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#include <mp-units/core.h>
namespace qcsv {
template<typename T>
concept parsable = std::default_initializable<T>
and requires (std::istream& is, T& t) { { is >> t } -> std::same_as<std::istream&>; };
consteval auto nsdms(std::meta::info type) -> std::vector<std::meta::info>
{
return std::meta::nonstatic_data_members_of(type, std::meta::access_context::current());
}
template<typename T>
consteval auto all_parsable_quantities_in() -> bool
{
constexpr auto [...dms] = [:std::meta::reflect_constant_array(nsdms(^^T)):];
return (... and (mp_units::Quantity<typename[:std::meta::type_of(dms):]>
and parsable<typename[:std::meta::type_of(dms):]::rep>));
}
template<typename T>
concept aggregate_of_quantities = std::meta::is_aggregate_type(^^T) and all_parsable_quantities_in<T>();
enum class parse_error_code {
format_error,
missing_value,
extra_value
};
struct parse_error {
parse_error_code code;
std::size_t column_number = 0; // no nulo para format_error y missing_values, exclusivamente
};
namespace detail {
template<parsable... Ts>
requires (sizeof...(Ts) > 0)
[[nodiscard]] auto parse_numerical_values(
std::string_view csv_line,
char delim = ','
) -> std::expected<std::tuple<Ts...>, parse_error>
{
using enum parse_error_code;
auto res = std::tuple<Ts...>{};
auto isstr = std::ispanstream{csv_line};
auto column = 1uz;
auto parse_one = [&](auto& value) -> std::expected<void, parse_error> {
isstr >> std::ws; // saltamos espacios
if (not (isstr >> value)) { // si no hay valor o éste es inválido
return std::unexpected{parse_error{isstr.eof()? missing_value : format_error, column}};
}
isstr >> std::ws; // consumimos los espacios post-valor
auto next = isstr.peek(); // leemos el siguiente carácter
if (next == delim){ // si es igual al delimitador
if (column == sizeof...(Ts)) { return std::unexpected{parse_error{extra_value}}; }
isstr.ignore(); // saltamos el delimitador para la siguiente columna
++column;
}
else if (next != std::char_traits<char>::eof()) {
// el carácter no es el delimitador ni el final del stream, es basura (e.g., 'a' en "55 a ,"):
return std::unexpected{parse_error{format_error, column}};
}
return {};
};
// parseamos valor a valor, interrumpiendo el proceso de producirse un error:
auto status = std::expected<void, parse_error>{};
std::apply([&](auto&... vs){
((status = parse_one(vs), status.has_value()) and ...);
}, res);
// emitimos error en cuanto un parseo resulta incorrecto:
if (not status) { return std::unexpected{status.error()}; };
// emitimos error si después de completar la tupla aún quedan valores por procesar:
if (not (isstr >> std::ws).eof()) { return std::unexpected{parse_error{extra_value}}; }
return res;
}
template<mp_units::Quantity Q>
using get_rep_t = typename Q::rep;
template<aggregate_of_quantities T>
[[nodiscard]] auto parse_quantities(
std::string_view csv_line,
char delim = ','
) -> std::expected<T, parse_error>
{
// obtenemos el conjunto de magnitudes en el agregado:
static constexpr auto dms = std::define_static_array(nsdms(^^T));
// así como sus tipos subyacentes:
static constexpr auto reps = dms
| std::views::transform([](std::meta::info m){
return std::meta::substitute(^^get_rep_t, {std::meta::type_of(m)});
});
// meta-valor para el correspondiente parseador de tipos subyacentes:
constexpr auto parser_refl = std::meta::substitute(^^detail::parse_numerical_values, reps);
// parseamos los valores numéricos y los asociamos a las cantidades con las unidades correspondientes:
return [:parser_refl:](csv_line, delim)
.transform([](auto const& values) {
return []<std::size_t... Idx>(auto const& vs, std::index_sequence<Idx...>) {
auto make_member = [&]<std::size_t I>{
using M = typename[:std::meta::type_of(dms[I]):];
return M{std::get<I>(vs), M::unit};
};
return T{make_member.template operator()<Idx>()...};
}(values, std::make_index_sequence<dms.size()>{});
}
);
}
[[nodiscard]] inline auto getlines(
std::istream& is,
std::string& buffer
) -> std::generator<std::string_view>
{
while (std::getline(is, buffer)) {
co_yield std::string_view{buffer};
}
}
} // detail namespace
enum class file_error_code {
invalid_extension,
unable_to_open_file
};
struct csv_error {
std::variant<file_error_code, parse_error> code;
std::size_t line_number = 0; // no nulo para parse_error, exclusivamente
};
template<aggregate_of_quantities T>
[[nodiscard]] auto load_quantities(
std::filesystem::path const& pth, // dirección del fichero CSV
bool drop_header = true, // ¿existe una cabecera que ignorar?
char delim = ',' // delimitador de valores (coma por defecto)
) -> std::expected<std::vector<T>, csv_error>
{
using enum file_error_code;
if (auto const ext = pth.extension(); ext != ".csv" and ext != ".CSV") {
return std::unexpected(csv_error{invalid_extension});
}
auto csv = std::ifstream{pth};
if (!csv) { return std::unexpected(csv_error{unable_to_open_file}); }
auto res = std::vector<T>{};
for (
auto buffer = std::string{};
auto&& [n, ln] : detail::getlines(csv, buffer)
| std::views::drop(drop_header? 1 : 0)
| std::views::enumerate
) {
auto parsed_data = detail::parse_quantities<T>(ln, delim);
if (not parsed_data) {
return std::unexpected(csv_error{
.code = parsed_data.error(),
.line_number = static_cast<std::size_t>(drop_header? n + 2 : n + 1)
});
}
res.push_back(std::move(*parsed_data));
}
return res;
}
[[nodiscard]] inline auto process_csv_error(csv_error const& err) -> std::string
{
auto message = std::string{"CSV error: "};
err.code.visit([&](auto&& e) {
using T = std::remove_cvref_t<decltype(e)>;
if constexpr (std::is_same_v<T, file_error_code>) {
using enum file_error_code;
switch (e) {
case invalid_extension:
message += "la extension del archivo no es .csv"; break;
case unable_to_open_file:
message += "no se pudo abrir el archivo (por permisos o inexistencia)"; break;
}
}
else if constexpr (std::is_same_v<T, parse_error>) {
using enum parse_error_code;
if (e.column_number > 0) {
message += std::format("columna {}, ", e.column_number);
}
message += std::format("línea {}: ", err.line_number);
switch (e.code) {
case format_error: message += "formato numérico inválido"; break;
case missing_value: message += "dato ausente"; break;
case extra_value: message += "número de columnas mayor al esperado"; break;
}
}
});
return message;
}
} // qcsv namespace
#endif // QUANTITY_CSV_HPP
Referencias bibliográficas
- cppreference – Zero-overhead principle – https://en.cppreference.com/cpp/language/Zero-overhead_principle
- mp-units – https://mpusz.github.io/mp-units/latest/
- P3045 – Quantities and units library – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3045r7.html
- mp-units – Safety features – https://mpusz.github.io/mp-units/HEAD/getting_started/safety_features/
- Wikipedia – Mars Climate Orbiter – https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
- Wikipedia – Levenberg–Marquardt algorithm – https://en.wikipedia.org/wiki/Levenberg%E2%80%93Marquardt_algorithm
- Dlib – Optimization – https://dlib.net/dlib/optimization/
- Matplot++ – https://alandefreitas.github.io/matplotplusplus/




No hay comentarios:
Publicar un comentario