NASA气象卫星意外坠落,原因竟是计量单位转换这样的“小问题”。为编程语言添加对计量单位的支持可以很大程度上避免这样的错误,编程任务也变得更有趣。F#提供了对计量单位的静态检查,并且封装了国际单位制的各个单位和物理常量,另外我们也可以定义自己的单位;在单位之间进行换算也很简单;此外F#还支持计量单位的泛型。作为对NASA气象卫星的纪念,本文最后给出了一个模拟太阳系的例子 :)
“失足”的NASA气象卫星
1998年2月,美国宇航局(NASA)发射了一枚探测火星气象的卫星,预定于1999年9月23日抵达火星。然而研究人员惊讶地发现,卫星没有进入预定的轨道,却陷入了火星大气层,很快就烟消云散了。NASA的官员经过紧急调查,发现问题居然出在有些资料的计量单位没有把英制(English)转换成公制(Metric),错误起自承包工程的洛克希德马丁航天公司。美国企业包括太空工业使用英制,喷射推进实验室(国家实验室)使用公制,承包商理应把英制都转换成公制,以便喷射推进实验室每天两次启动小推进器,来调整太空船的航向。导航员认定启动小推进器的力是以公制的"牛顿"为单位。不料,洛克希德马丁公司提供的资料却是以英制的"磅"为单位,结果导致太空船的航向出现微小偏差。日积月累,终于差之毫厘,失之千里。
这个英制未换算成公制的"小错误"造成的损失有多大呢?其他损失不计,单单卫星的造价就高达1.25亿美元,这些费用就这样全泡了汤。理论上,如果使用统一的度量衡计量单位制,这样的损失本是可以避免的。
何为计量单位
早上醒来,看到天气预报今天温度是4-12 ℃,不算太冷;离班车发车只有20分钟了,得抓紧点;午饭几个同事一起出去吃,每个人10块钱;晚上回来下车后,去水果摊买了几个苹果,是10块钱3斤的那种;回去一看电费单又来了,用了60度电……,我们每天都得跟“单位” 打交道。
所谓单位(Unit)是指给定的某一基础量,它通常伴随着某种表示方法,例如米、秒、公斤等,以方便人们在沟通某一量时有共通的概念。而计量单位(Unit of Measurement)是单位的具体统称,为人们计算一个数额的方法。如长度单位可以小至纳米、微米、毫米,也可以大到千米、光年等;时间单位可以是微秒、秒、分钟到日、周、月、年等等。
基本单位与导出单位
各种物理量都有它们的量度单位,并以选定的物质在规定条件下所显示的数量作为基本量度单位的标准,在不同时期和不同的学科中,基本量的选择可以不同。如物理学上以时间、长度、质量、温度、电流强度、发光强度、物质的量这7个物理单位为基本量,它们的单位依次为:秒、米 (单位)、千克、开尔文、安培、坎德拉、摩尔(可以翻一下高中物理课本,呵呵)。
由基本量根据有关公式推导出来的其他量,叫做导出量。导出量的单位叫做导出单位。如速度的单位是由长度和时间单位组成的用“m/s”表示。
单位的换算
不同的国家或文化中,单位制可能不同,如我们常用的“尺”(1米=3尺)、英尺(英制单位)、米(公制单位)等等。现在NBA中球员的身高一般仍使用英制单位表示,如麦迪的身高是6英尺8英寸,也就是2米03。你可以说一个人的三围是34、24、34(英寸),也可以说是86、61、86(厘米)。两个人进行交流时,显然使用统一的计量单位会更好些,NASA气象卫星就是一个好例子。
F#对计量单位的支持
显然NASA使用的编程语言不支持计量单位,于是很多人希望扩展语言以添加对计量单位的静态检查。F#的CTP(2008年9月)版本提供了单位的静态检查和推导功能。这些功能有趣而且实用。
先来看一下如何定义一个单位。
F# Code - 定义计量单位
[<Measure>]
type kg
[<Measure>]
type s
[<Measure>]
type m
注意Measure特性,这里通过它定义了三个单位,分别是千克、秒和米。虽然这里使用了type关键字,但是这里仅仅用于创建计量单位,通过Reflector可以看到,根本没有kg、s和m的踪影。来看看如何使用这些单位。
F# Code - 使用计量单位
let heightOfMyOfficeWindow = 3.5<m> // -> float<m>
let gravityOnEarch = 9.81<m/s^2> // -> float<m/s^2>
这里定义了两个量,一个表示高度,单位为米,一个表示重力加速度,单位为米/秒2。(这里的m/s^2也可写作m s^-2或m/s/s,从数学意义上来看都是等价的)好,现在考虑下,如果从窗户跳下去,做自由落体运动,落地时速度会有多少:
F# Code
let speedOfImpact = sqrt (2.0 * gravityOnEarch * heightOfMyOfficeWindow)
// speedOfImpact: float<m/s>
print_any speedOfImpact // 8.28673639 m/s
计算结果为约8.3m/s,貌似很疼吧!这里最神奇的地方在于,F#竟然知道speedOfImpact的单位是m/s,太酷了!看计算公式 sqrt (m/s^2 * m),这样更容易理解些。如果将两个单位不同的量相加,F#会报告错误:
再来计算一下我受到的重力是多少:
F# Code - 计算重力
let myMass = 75.0<kg>
let forceOnGround = myMass * gravityOnEarch // -> kg m/s^2
forceOnGround的单位是 kg m/s^2,即物理学中力的单位:牛顿。它是导出单位的一个例子,我们可以为它建立一个新的单位:
F# Code - 导出单位
[<Measure>]
type N = kg m/s^2
let forceOnGround: float<N> = myMass * gravityOnEarch // -> N
导出单位的作用就像类型的别名一样,所以在F#中,N和kg m/s^2是一样的。
F#与国际单位制
在F#的FSharp.PowerPack.dll中包含了国际单位制中的基本单位和导出单位,其模块是Microsoft.FSharp.Math.SI(SI即国际单位制的缩写),也包含了物理常量,在PhysicalConstants模块中。来看看如何计算万有引力:
F# Code - 计算万有引力
open Microsoft.FSharp.Math
open SI
let gravitationalForce
(d: float<m>)
(m1: float<kg>)
(m2: float<kg>) : float<N> =
m1 * m2 / (d * d) * PhysicalConstants.G
这里的m、kg和N都定义在SI中,G是引力常量。
F#中的单位换算
如果每个人都用标准单位就好了。但F#也允许你使用其它单位,还是考虑那个自由落体运动,这次单位是英尺(feet)。
F# Code - 使用非公制单位
[<Measure>]
type ft
let gravityOnEarch = 32.2<ft/s^2>
let heightOfMyOfficeWindow = 11.5<ft>
let speedOfImpact = sqrt (2.0 * gravityOnEarch * heightOfMyOfficeWindow)
此时速度speedOfImpact的单位就是ft/s了。如果需要在英尺和米之间进行转换的话,可以先定义一个转换因子,然后在进行转换:
F# Code - 单位换算
let feetPerMetre = 3.28084<ft/m>
let heightOfMyOfficeWindowInMetres =
heightOfMyOfficeWindow / feetPerMetre
事实上,F#认为ft和m没有任何关系,所以转换因子有我们开发人员来定。可以认为转换因子就是定义了一个常量,这样出错的几率会减少很多。另一种方式是将转换因子封装在计量单位中,如:
F# Code - 封装转换因子
[<Measure>]
type ft =
static member perMetre = 3.28084<ft/m>
需要注意的是,此时ft已经成为真正的类型,perMetre是它的一个静态属性。
另外,如何将一个单纯的数字转换带有单位的量呢?可以给它乘上一个单位量,如:
F# Code - 使用单位量
let rawString = Console.ReadLine()
let rawFloat = Double.Parse(rawString)
let timeInSeconds = rawFloat * 1.0<s>
timeInSeconds的类型是float<s>,如果要将它重新转换为纯数字,可以让它除以1.0<s>:
F# Code
let timeSpan = TimeSpan.FromSeconds(timeInSeconds / 1.0<s>)
注意,timeInSeconds / 1.0<s>的类型为float<1>,这种数字称为无因次量,即没有单位的量。
计量单位也泛型
计量单位也能与泛型扯上关系?考虑下面的函数:
F# Code
let sqr x = x * x
sqr的类型为int -> int,F#认为它的参数和返回类型都是int,应用类型标注,可以给参数指定类型:
F# Code
let sqrMass (x: float<kg>) = x * x
let sqrLength (x: float<m>) = x * x
let sqrSpeed (x: float<m/s>) = x * x
闻到坏味了吗?难道要为每个单位都写一个函数?泛型的需求就出来了。可以这么来写:
F# Code - 泛型计量单位
let sqr (x: float<_>) = x * x
sqr的类型为float<’u> -> float<’u^2>,’u表示sqr接受任何单位的值,包括float类型本身。如果我们要求出一个列表所有元素的和,我们可以使用List模块的fold函数,此时也可以使用泛型单位:
F# Code - 泛型单位值
let sum xs = List.fold_left (+) 0.0<_> xs
sum的类型为float<’u> list -> float<’u>。
应用
为了纪念一下消失在火星大气层的NASA气象卫星,我们这里使用F#模拟一下太阳系(Solar System),这个例子来自MSDN的F# Samples。
F# Code - 模拟太阳系
//----------------------------------------------------------------------------
// A Simple Solar SYstem Simulator, using Units of Measure
//
// Copyright (c) Microsoft Corporation 2005-2008.
//
// This sample code is provided "as is" without warranty of any kind.
// We disclaim all warranties, either express or implied, including the
// warranties of merchantability and fitness for a particular purpose.
//----------------------------------------------------------------------------
#light
open System
open System.Windows.Forms
open System.Drawing
open Microsoft.FSharp.Control.CommonExtensions
//-----------------------------------------------
// Graphics System
// Define a special type of form that doesn't flicker
type SmoothForm() as x =
inherit Form()
do x.DoubleBuffered <- true
let form = new SmoothForm(Text="F# Solar System Simulator", Visible=true, TopMost=true, Width=500, Height=500)
type IPaintObject =
abstract Paint : Graphics -> unit
// Keep a list of objects to draw
let paintObjects = new ResizeArray<IPaintObject>()
form.Paint.Add (fun args ->
let g = args.Graphics
// Clear the form
g.Clear(color=Color.Blue)
// Draw the paint objects
for paintObject in paintObjects do
paintObject.Paint(g)
// Invalidate the form again in 10 milliseconds to get continuous update
async { do! System.Threading.Thread.AsyncSleep(10)
form.Invalidate() } |> Async.Spawn
)
// Set things going with an initial Invaldiate
form.Invalidate()
//-----------------------------------------------
[<Measure>]
type m
[<Measure>]
type km
[<Measure>]
type AU
[<Measure>]
type sRealTime
[<Measure>]
type s
[<Measure>]
type kg
[<Measure>]
type pixels
type System.TimeSpan with
member x.TotalSecondsTyped = (box x.TotalSeconds :?> float<sRealTime>)
let G = 6.67e-11<m ^ 3 / (kg s^2)>
let m_per_AU = 149597870691.0<m/AU>
let AU_per_m = 1.0/m_per_AU
let Pixels_per_AU = 200.0<pixels/AU>
let m_per_km = 1000.0<m/km>
let AU_per_km = m_per_km * AU_per_m
// Make 5 seconds into one year
let sec_per_year = 60.0<s> * 60.0 * 24.0 * 365.0
// One second of real time is 1/40th of a year of model time
let realTimeToModelTime (x:float<sRealTime>) = float x * sec_per_year / 80.0
let pixels (x:float<pixels>) = int32 x
type Planet(ipx:float<AU>,ipy:float<AU>,
ivx:float<AU/s>,ivy:float<AU/s>,
brush:Brush,mass:float<kg>,
width,height) =
// For this sample e store the simulation state directly in the object
let mutable px = ipx
let mutable py = ipy
let mutable vx = ivx
let mutable vy = ivy
member p.Mass = mass
member p.X with get() = px and set(v) = (px <- v)
member p.Y with get() = py and set(v) = (py <- v)
member p.VX with get() = vx and set(v) = (vx <- v)
member p.VY with get() = vy and set(v) = (vy <- v)
interface IPaintObject with
member obj.Paint(g) =
let rect = Rectangle(x=pixels (px * Pixels_per_AU)-width/2,
y=pixels (py * Pixels_per_AU)-height/2,
width=width,height=height)
g.FillEllipse(brush,rect)
type Simulator() =
// Get the start time for the animation
let startTime = System.DateTime.Now
let lastTimeOption = ref None
let ComputeGravitationalAcceleration (obj:Planet) (obj2:Planet) =
let dx = (obj2.X-obj.X)*m_per_AU
let dy = (obj2.Y-obj.Y)*m_per_AU
let d2 = (dx*dx) + (dy*dy)
let d = sqrt d2
let g = obj.Mass * obj2.Mass * G /d2
let ax = (dx / d) * g / obj.Mass
let ay = (dy / d) * g / obj.Mass
ax,ay
/// Find all the gravitational objects in the system except the given object
let FindObjects(obj) =
[ for paintObject in paintObjects do
match paintObject with
| :? Planet as p2 when p2 <> obj ->
yield p2
| _ ->
yield! [] ]
member sim.Step(time:TimeSpan) =
match !lastTimeOption with
| None -> ()
| Some(lastTime) ->
for paintObject in paintObjects do
match paintObject with
| :? Planet as obj ->
let timeStep = (time - lastTime).TotalSecondsTyped |> realTimeToModelTime
obj.X <- obj.X + timeStep * obj.VX
obj.Y <- obj.Y + timeStep * obj.VY
// Find all the gravitational objects in the system
let objects = FindObjects(obj)
// For each object, apply its gravitational field to this object
for obj2 in objects do
let (ax,ay) = ComputeGravitationalAcceleration obj obj2
obj.VX <- obj.VX + timeStep * ax * AU_per_m
obj.VY <- obj.VY + timeStep * ay * AU_per_m
| _ -> ()
lastTimeOption := Some time
member sim.Start() =
async { while true do
let time = System.DateTime.Now - startTime
// Sleep a little to give better GUI updates
do! System.Threading.Thread.AsyncSleep(1)
sim.Step(time) }
|> Async.Spawn
let s = Simulator().Start()
let massOfEarth = 5.9742e24<kg>
let massOfMoon = 7.3477e22<kg>
let massOfMercury = 3.3022e23<kg>
let massOfVenus = 4.8685e24<kg>
let massOfSun = 1.98892e30<kg>
let mercuryDistanceFromSun = 57910000.0<km> * AU_per_km
let venusDistanceFromSun = 0.723332<AU>
let distanceFromMoonToEarth =384403.0<km> * AU_per_km
let orbitalSpeedOfMoon = 1.023<km/s> * AU_per_km
let orbitalSpeedOfMecury = 47.87<km/s> * AU_per_km
let orbitalSpeedOfVenus = 35.02<km/s> * AU_per_km
let orbitalSpeedOfEarth = 29.8<km/s> * AU_per_km
let sun = new Planet(ipx=1.1<AU>,
ipy=1.1<AU>,
ivx=0.0<AU/s>,
ivy=0.0<AU/s>,
brush=Brushes.Yellow,
mass=massOfSun,
width=20,
height=20)
let mercury = new Planet(ipx=sun.X+mercuryDistanceFromSun,
ipy=sun.Y,
ivx=0.0<AU/s>,
ivy=orbitalSpeedOfMecury,
brush=Brushes.Goldenrod,
mass=massOfMercury,
width=10,
height=10)
let venus = new Planet(ipx=sun.X+venusDistanceFromSun,
ipy=sun.Y,
ivx=0.0<AU/s>,
ivy=orbitalSpeedOfVenus,
brush=Brushes.BlanchedAlmond,
mass=massOfVenus,
width=10,
height=10)
let earth = new Planet(ipx=sun.X+1.0<AU>,
ipy=sun.Y,
ivx=0.0<AU/s>,
ivy=orbitalSpeedOfEarth,
brush=Brushes.Green,
mass=massOfEarth,
width=10,
height=10)
let moon = new Planet(ipx=earth.X+distanceFromMoonToEarth,
ipy=earth.Y,
ivx=earth.VX,
ivy=earth.VY+orbitalSpeedOfMoon,
brush=Brushes.White,
mass=massOfMoon,
width=2,
height=2)
paintObjects.Add(sun)
paintObjects.Add(mercury)
paintObjects.Add(venus)
paintObjects.Add(earth)
paintObjects.Add(moon)
form.Show()
#if COMPILED
[<STAThread>]
do Application.Run(form)
#endif
效果图是这样:
中间黄色的是太阳,按距离由近及远分别是水星、金星和地球,地球上的小点儿是月球。
小结
NASA气象卫星意外坠落,原因竟是计量单位转换这样的“小问题”。为编程语言添加对计量单位的支持可以很大程度上避免这样的错误,编程任务也变得更有趣。F#提供了对计量单位的静态检查,并且封装了国际单位制的各个单位和物理常量,另外我们也可以定义自己的单位;在单位之间进行换算也很简单;此外F#还支持计量单位的泛型。作为对NASA气象卫星的纪念,本文最后给出了一个模拟太阳系的例子 :)
(要了解本人所写的其它F#随笔请查看 F#系列随笔索引)
参考:
从“失足”的NASA气象卫星说起——计量单位的故事;
http://en.wikipedia.org/wiki/Units_of_measurement(中文);
Units of Measure in F#:Part 1, Part 2, Part 3;
F# Samples;