1+ ^{:kindly/hide-code true
2+ :clay
3+ {:title " Simulating 1-D Convection in Clojure — From Equations to Arrays"
4+ :quarto {:author :syungb
5+ :description " A quick exploration to simulate a classic fluid dynamics equation in Clojure using Java arrays."
6+ :type :post
7+ :date " 2025-07-15"
8+ :category :clojure
9+ :tags [:computationalfluiddynamics :cfd :python :conversion ]}}}
10+ (ns scicloj.cfd.intro.linear-1d-convection-with-array
11+ (:require
12+ [scicloj.kindly.v4.api :as kindly]
13+ [scicloj.kindly.v4.kind :as kind]))
14+
15+ ^:kindly/hide-code
16+ (def tex (comp kindly/hide-code kind/tex))
17+ ^:kindly/hide-code
18+ (def compose-plot-data #(into [] (map (fn [x u] (hash-map :x x :y u)) % %2 )))
19+ ; ;
20+ ; ; Earlier this year I gave a [talk](https://youtu.be/RXr9i-aw0lM?si=NpnGOHh8TjC1gufI) at the first online
21+ ; ; [Scinoj Light Conference](https://scicloj.github.io/scinoj-light-1/), sharing a [ongoing project](https://github.com/scicloj/cfd-python-in-clojure)
22+ ; ; to port [Computational Fluid Dynamics(CFD) learning materials](https://github.com/barbagroup/CFDPython)
23+ ; ; from Python to Clojure.
24+ ; ;
25+ ; ; In this post, I'll demonstrate a simple one-dimensional linear [convection](https://en.wikipedia.org/wiki/Convection) simulation implemented
26+ ; ; in Clojure using Java primitive arrays. This example shows a glimpse into the porting effort,
27+ ; ; with a focus on expressing numerical simulations using only built-in Clojure functions.
28+ ; ;
29+ ; ; ## The Equation
30+ ; ;
31+ ; ; _(This section won't take up too much of your time...)_
32+ ; ;
33+ ; ; We're going to simulate the 1-D linear convection equation:
34+ ; ;
35+ (tex " \\ frac{\\ partial u }{\\ partial t} + c \\ frac{\\ partial u}{\\ partial x} = 0" )
36+ ; ;
37+ ; ; This explains how the flow velocity `u` changes over time `t` and position `x`,
38+ ; ; with `c` which is the wave speed.
39+ ; ;
40+ ; ; Instead of focusing on the physical interpretation(since I am no expert),
41+ ; ; the main focus on this post will be its implementation side - expressing it numerically and coding it in **Clojure**!
42+ ; ;
43+ ; ; Using a finite-difference scheme, the discretized form becomes:
44+ ; ;
45+ (tex " \\ frac{u_i^{n+1} - u_i^n}{\\ Delta t} + c \\ frac{u_i^n - u_{i-1}^n}{\\ Delta x} = 0" )
46+ ; ;
47+ ; ; Then, solving for `u_i^{n+1}` gives:
48+ ; ;
49+ (tex " u_i^{n+1} = n_i^n - c \\ frac{\\ Delta t}{\\ Delta x}(u_i^n - u_{i-1}^n)" )
50+ ; ;
51+ ; ; ## Initial Setup
52+ ; ;
53+ ; ; We begin by defining initial simulation parameters to start:
54+ ; ;
55+ ; ; * nx: number of sliced steps for spatial point `x` from `x-start` and `x-end`
56+ ; ; * nt: number of time steps we want to propagate
57+ ; ; * dx: each sliced `x` calculated from `dx = (x-end - x-start) / (nx - 1)`
58+ ; ; * dt: sliced each time step
59+ ; ; * c: speed of wave
60+ ; ;
61+ (def init-params
62+ {:x-start 0
63+ :x-end 2
64+ :nx 41
65+ :nt 25
66+ :dx (/ (- 2 0 ) (dec 41 ))
67+ :dt 0.025
68+ :c 1 })
69+ ; ;
70+ ; ; ### Creating the `x` Grid
71+ ; ;
72+ ; ; With the given `init-params` we defined earlier, we create a float-array of spatial points `x`:
73+ ; ;
74+ (def array-x (let [{:keys [nx x-start x-end]} init-params
75+ arr (float-array nx)
76+ step (/ (- x-end x-start) (dec nx))]
77+ (dotimes [i nx]
78+ (aset arr i (float (* i step))))
79+ arr))
80+ ; ;
81+ ^:kindly/hide-code array-x
82+ ; ;
83+ ; ; ### Defining Initial Flow Velocity Condition
84+ ; ;
85+ ; ; The initial flow velocity(when `t = 0`) is 2 when `x ∈ [0.5, 1.0]`, and is 1 elsewhere:
86+ ; ;
87+ (def init-cond-fn #(float (if (and (>= % 0.5 ) (<= % 1.0 )) 2 1 )))
88+ ; ;
89+ (def array-u
90+ (let [nx (:nx init-params)
91+ u (float-array nx)]
92+ (dotimes [i nx]
93+ (let [x-i (aget array-x i)
94+ u-i (init-cond-fn x-i)]
95+ (aset u i u-i)))
96+ u))
97+
98+ (def init-array-u (float-array array-u))
99+ ^:kindly/hide-code init-array-u
100+ ; ;
101+ ; ; We can visualize this initial `u`:
102+ ^:kindly/hide-code
103+ (kind/vega-lite
104+ {:mark " line"
105+ :width 500 :height 300
106+ :encoding {:x {:field " x" :type " quantitative" }
107+ :y {:field " y" :type " quantitative" }}
108+ :data {:values (compose-plot-data array-x init-array-u)}})
109+ ; ;
110+ ; ; ### Wait, Why `dotimes`?
111+ ; ;
112+ ; ; Since we're working with mutable Java arrays(float-array), `dotimes` is an efficient choice here.
113+ ; ; Because it gives direct, index-based iteration. And it pairs naturally with `aget` and `aset`
114+ ; ; for reading and writing array values.
115+ ; ;
116+ ; ; ## Implementing and the Simulation
117+ ; ;
118+ ; ; With the initial setup complete, we now apply the discretized convection equation at each time step.
119+ ; ;
120+ ; ; ### Step Function
121+ ; ;
122+ ; ; Given the previous time step's `array-u` and the `init-params`,
123+ ; ; We compute and mutate the flow velocity in-place:
124+ ; ;
125+ (defn mutate-linear-convection-u
126+ [array-u {:keys [nx c dx dt]}]
127+ (let [u_i (float-array array-u)]
128+ (dotimes [i (- nx 2 )]
129+ (let [idx (inc i)
130+ un-i (aget u_i idx)
131+ un-i-1 (aget u_i i)
132+ new-u-i (float (- un-i (* c (/ dt dx) (- un-i un-i-1))))]
133+ (aset array-u idx new-u-i))))
134+ array-u )
135+ ; ;
136+ ; ; ### Time Integration
137+ ; ;
138+ ; ; We run the step function `nt` times to run our simulation over time.
139+ ; ;
140+ (defn simulate!
141+ [array-u {:keys [nt] :as init-params}]
142+ (loop [n 0 ]
143+ (if (= n nt)
144+ array-u
145+ (do (mutate-linear-convection-u array-u init-params) (recur (inc n))))))
146+ ; ;
147+ ; ; Finally, we visualize the resulting `array-u`:
148+
149+ (simulate! array-u init-params)
150+
151+ ^:kindly/hide-code
152+ (kind/vega-lite
153+ {:mark " line"
154+ :width 500 :height 300
155+ :encoding {:x {:field " x" :type " quantitative" }
156+ :y {:field " y" :type " quantitative" }}
157+ :data {:values (compose-plot-data array-x array-u)}})
158+ ; ;
159+ ; ; The plot shows how the flow velocity shifts from left to right over time,
160+ ; ; while also becoming smoother. Nice!
161+ ; ;
162+ ; ; ## The Summary
163+ ; ;
164+ ; ; This Simple example demonstrates a simulation process using low-level Java primitive arrays in Clojure.
165+ ; ;
166+ ; ; Choosing this approach provided mutable, non-persistent data structures. While this deviates from idiomatic Clojure,
167+ ; ; it offers significant performance benefits for big-size numerical simulations.
168+ ; ; However, it comes with trade-offs; by opting for mutability, we give up the guarantees of immutability
169+ ; ; and structural sharing, making the code less safe and more error-prone.
170+ ; ;
171+ ; ; As the porting project continues, we plan to evolve the design to better align with idiomatic Clojure principles.
172+ ; ; Stay tuned!
173+
174+ (comment
175+ (require '[scicloj.clay.v2.api :as clay])
176+ (clay/make! {:source-path " scicloj/cfd/intro/linear_1d_convection_with_array.clj" }))
0 commit comments