You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在CodeWorld的物理模拟中实现动画与实时数据图表的联动?

解决方案:在CodeWorld水箱模拟中实现实时数据采集与动态图表

你这个需求在CodeWorld里完全可以通过扩展状态模型来实现,因为activityOf的状态参数并不局限于单个数值——我们可以把它改成包含当前液位、累计时间和采样数据的复合类型,这样就能同时跟踪模拟状态和历史数据了。下面分三个部分给出具体实现:


1. 扩展状态以实时收集采样数据

首先,把原来的单个Double类型的状态,改成一个三元组:

type SimulationState = (Double, Double, [(Double, Double)])
-- 分别对应:当前液位h,累计时间t,采样点列表[(时间, 液位)]

然后修改initial函数,初始化状态时加入初始时间和第一个采样点:

initial(rs) = (5.0, 0.0, [(0.0, 5.0)])  -- 初始h=5,时间t=0,第一个采样点(0,5)

接下来调整change函数,处理TimePassing事件时,不仅更新液位,还要累计时间,并按固定间隔采样(比如每0.5秒采一次,避免数据爆炸):

-- 采样间隔(秒),可根据需求调整
sampleInterval = 0.5

change :: SimulationState -> Event -> SimulationState
change (h, t, samples) (TimePassing dt) =
    let newT = t + dt
        newH = max (h - qout(h)*dt + qin*dt) 0.0
        -- 检查是否到达下一个采样点
        nextSampleTime = case samples of
            [] -> sampleInterval
            (lastT, _):_ -> lastT + sampleInterval
        -- 如果当前时间超过下一个采样点,就添加新数据
        newSamples = if newT >= nextSampleTime
                     then samples ++ [(newT, newH)]
                     else samples
    in (newH, newT, newSamples)
-- 其他事件不改变状态
change state _ = state

2. 动态更新h(t)演化图表

现在我们可以从状态中取出采样数据,动态生成图表,替换原来的手动polyline:

首先调整tank_water函数,接收SimulationState作为参数,然后从状态中提取h和采样数据:

tank_water :: SimulationState -> Picture
tank_water (h, _, samples) = composedOf [
    tank,
    water(h),
    dynamicGraph samples,  -- 动态生成的图表
    coordinatePlane
    ]

然后实现dynamicGraph函数,把采样数据转换成适合展示的折线图(可以调整位置和缩放比例,让图表和水箱对齐):

dynamicGraph :: [(Double, Double)] -> Picture
dynamicGraph samples = translated (scaled (0.5, 1) (polyline samples)) 5 0
-- 解释:
-- scaled(0.5,1):把时间轴压缩一半,避免图表太长
-- translated 5 0:把图表放在水箱右侧(原水箱在position=-5的位置)

这样,模拟运行时,图表就会随着采样数据的增加实时更新了。


3. 导出模拟数据到文件(通用方案)

如果需要把数据保存到文件,分两种场景:

场景1:浏览器版CodeWorld

浏览器环境没法直接写文件,但可以通过生成CSV数据并触发下载的方式实现。我们可以添加一个按钮事件,当用户点击时导出数据:

首先,定义一个导出CSV的JS调用函数(CodeWorld支持嵌入JS逻辑):

import CodeWorld

exportCSV :: [(Double, Double)] -> IO ()
exportCSV samples = unsafeJS $ \samples ->
    let csv = "Time,Height\n" ++ samples.map((t,h) => t + "," + h).join("\n")
        blob = new Blob([csv], {type: "text/csv"})
        url = URL.createObjectURL(blob)
        a = document.createElement("a")
        a.href = url
        a.download = "tank_data.csv"
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
        URL.revokeObjectURL(url)

然后在change函数里添加对KeyPress事件的处理(比如按D键导出):

change state@(_, _, samples) (KeyPress "D") =
    do
        unsafePerformIO $ exportCSV samples
        state
change state other = originalChange state other  -- 这里的originalChange是之前的状态更新逻辑

场景2:本地Haskell环境运行CodeWorld

如果是在本地用Haskell编译运行,直接用System.IO的writeFile就能把数据写入文件,非常简单:

import System.IO

change state@(_, _, samples) (KeyPress "D") =
    do
        let csv = unlines $ "Time,Height" : map (\(t, h) -> show t ++ "," ++ show h) samples
        writeFile "tank_simulation_data.csv" csv
        state

完整修改后的代码

把所有部分整合起来,完整代码如下:

import CodeWorld
import System.IO (writeFile)

program = activityOf(initial, change, tank_water)
composedOf = pictures

-- 定义复合状态类型
type SimulationState = (Double, Double, [(Double, Double)])  -- (当前h, 累计时间t, 采样点列表)

-- 初始值与参数
initial(rs) = (5.0, 0.0, [(0.0, 5.0)])
g = 9.81
area = 0.015
qin = 0.0
sampleInterval = 0.5  -- 采样间隔(秒)

-- 出口流量函数
qout(h) = area*sqrt(2*g*max(h,0))

-- 状态更新逻辑
change :: SimulationState -> Event -> SimulationState
change (h, t, samples) (TimePassing dt) =
    let newT = t + dt
        newH = max (h - qout(h)*dt + qin*dt) 0.0
        -- 计算下一个采样时间
        nextSampleTime = case last samples of
            (lastT, _) -> lastT + sampleInterval
        -- 更新采样数据
        newSamples = if newT >= nextSampleTime
                     then samples ++ [(newT, newH)]
                     else samples
    in (newH, newT, newSamples)
-- 处理导出数据的按键事件(按D键)
change state@(_, _, samples) (KeyPress "D") =
    do
        let csv = unlines $ "Time,Height" : map (\(t, h) -> show t ++ "," ++ show h) samples
        unsafePerformIO $ writeFile "tank_data.csv" csv
        state
-- 其他事件不改变状态
change state _ = state

-- 系统动画
tank_water :: SimulationState -> Picture
tank_water (h, _, samples) = composedOf [
    tank,
    water(h),
    dynamicGraph samples,
    coordinatePlane
    ]

tank = translated(thickRectangle(width,height,0.2),position,4)
width = 3
height = 8
position = -5

water(h) = translated(colored(solidPolygon [(-width/2, 0), (-width/2, h), (width/2, h), (width/2,0)], light(blue)),position,0)

-- 动态生成h(t)图表
dynamicGraph :: [(Double, Double)] -> Picture
dynamicGraph samples = translated (scaled (0.5, 1) (polyline samples)) 5 0

这个方案的优势在于:

  • 状态设计通用,很容易扩展到其他需要数据采集的模拟场景
  • 采样间隔可配置,平衡数据精度和性能
  • 动态图表和模拟动画实时同步,非常适合教学演示
  • 数据导出功能支持本地和浏览器两种场景,满足后续分析需求

内容的提问来源于stack exchange,提问作者janpeter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 19:32:47