cons_laws/lib.rs
1//! Deterministic particle methods for 1D conservation laws.
2//!
3//! This crate implements the deterministic particle schemes described in the article
4//! *Entropy solutions of non-local scalar conservation laws with congestion via deterministic particle method*,
5//! E. Radici, F. Stra, (2021), [https://arxiv.org/abs/2107.10760](https://arxiv.org/abs/2107.10760).
6//!
7//! You can cite the article as
8//!
9//! ```text
10//! @online{RadiciStra2021,
11//! title={Entropy solutions of non-local scalar conservation laws with congestion via deterministic particle method},
12//! author={Emanuela Radici and Federico Stra},
13//! year={2021},
14//! eprint={2107.10760},
15//! archivePrefix={arXiv},
16//! primaryClass={math.AP},
17//! url={https://arxiv.org/abs/2107.10760}
18//! }
19//! ```
20//!
21//! This is a reimplementation in Rust of the Julia package
22//! [ConservationLawsParticles.jl](https://github.com/FedericoStra/ConservationLawsParticles.jl).
23//!
24//! # What's the goal?
25//!
26//! The conservation law to be solved is
27//!
28//! $$
29//! \partial_t\rho(t,x) + \mathop{\mathrm{div}}_x\bigl[\rho(t,x)v\bigl(\rho(t,x)\bigr)\bigl(V(t,x)-(\partial_xW*\rho)(t,x)\bigr)\bigr] = 0 ,
30//! $$
31//!
32//! where
33//!
34//! - $V:[0,\infty)\times\mathbb{R}\to\mathbb{R}$ is the external velocity,
35//! - $W:[0,\infty)\to\mathbb{R}$ is the interaction potential,
36//! - $v:[0,\infty)\to[0,\infty)$ is a non increasing function to model congestion,
37//! - the convolution is performed in space only.
38//!
39//! The approach to solve the conservation law is by tracking the motion of $N+1$ particles
40//! $X=(x_0,x_1,\dots,x_N)$ such that between each consecutive pair of particles there is
41//! a fraction $1/N$ of the total mass.
42//!
43//! Two deterministic particle schemes are described: one with integrated interaction $(\mathrm{ODE}_I)$ and one with sampled interaction $(\mathrm{ODE}_S)$:
44//!
45//! ```math
46//! \begin{aligned}
47//! (\mathrm{ODE}_I): & \left\{\begin{aligned}
48//! x_i'(t) &= v_i(t) \bar U_i(t), \\
49//! \bar U_i(t) &= V\bigl(t,x_i(t)\bigr) - (W*\partial_x\bar\rho)\bigl(t,x_i(t)\bigr)
50//! = V\bigl(t,x_i(t)\bigr) - \sum_{j=0}^N(\rho_{j+1}(t) - \rho_j(t)) W\bigl(t,x_i(t)-x_j(t)\bigr), \\
51//! v_i(t) &= \begin{cases}
52//! v\bigl(\rho_i(t)\bigr), & \text{if } \bar U_i(t) < 0, \\
53//! v\bigl(\rho_{i+1}(t)\bigr), & \text{if } \bar U_i(t) \geq 0, \end{cases}
54//! \end{aligned}\right. \\
55//! (\mathrm{ODE}_S): & \left\{\begin{aligned}
56//! x_i'(t) &= v_i(t) \dot U_i(t), \\
57//! \dot U_i(t) &= V\bigl(t,x_i(t)\bigr) - (\partial_xW*\dot\rho)\bigl(t,x_i(t)\bigr)
58//! = V\bigl(t,x_i(t)\bigr) - \frac1N\sum_{j=0}^NW'\bigl(t,x_i(t)-x_j(t)\bigr), \\
59//! v_i(t) &= \begin{cases}
60//! v\bigl(\rho_i(t)\bigr), & \text{if } \dot U_i(t) < 0, \\
61//! v\bigl(\rho_{i+1}(t)\bigr), & \text{if } \dot U_i(t) \geq 0. \end{cases}
62//! \end{aligned}\right.
63//! \end{aligned}
64//! ```
65//!
66//! Here $\rho_i$ denotes the quantity $\frac1{N(x_i-x_{i-1})}$,
67//! $\bar\rho$ is the piecewise constant density $\sum_{i=1}^N \rho_i 1_{[x_{i-1},x_i]}$
68//! and $\dot\rho$ is the atomic measure $\frac1N \sum_{i=0}^N \delta_{x_i}$.
69//! Notice that $\dot\rho$ is not a probability.
70//!
71//! # Example
72//!
73//! This example requires the `ode_solver` feature.
74//!
75//! Consider the following external velocity, interaction potential and mobility:
76//!
77//! ```math
78//! \begin{aligned}
79//! V(t,x) &= \mathop{\mathrm{sign}}(7.5-t)
80//! [2 + 0.5\sin(2x) + 0.3\sin(2\sqrt2x) + 0.2\cos(2\sqrt7x)] , \\
81//! W(t,x) &= (1 + \sin(t)) (x^2 - |x|) , \\
82//! v(t,\rho) &= (1-\rho)_+ .
83//! \end{aligned}
84//! ```
85//!
86//! The initial datum $\rho_0 = 1_{[-1.5,1.5]}$ is discretized $N=201$ with equally spaced particles.
87//!
88#![cfg_attr(feature = "ode_solver", doc = "```no_run")]
89#![cfg_attr(not(feature = "ode_solver"), doc = "```ignore")]
90//! use cons_laws::*;
91//! use ode_solvers::{dopri5::Dopri5, SVector};
92//!
93//! #[inline]
94//! fn V(t: f64, x: f64) -> f64 {
95//! let sign = if t < 7.5 { 1.0 } else { -1.0 };
96//! let magn = 2.0 + 0.5 * (2.0 * x).sin() + 0.3 * (2.0 * 2.0f64.sqrt() * x).sin()
97//! + 0.2 * (2.0 * 7.0f64.sqrt() * x).cos();
98//! sign * magn
99//! }
100//! #[inline]
101//! fn Wprime(t: f64, x: f64) -> f64 { (x + x - x.signum()) * (1.0 + t.sin()) }
102//! #[inline]
103//! fn W(t: f64, x: f64) -> f64 { (x * x - x.abs()) * (1.0 + t.sin()) }
104//! #[inline]
105//! fn v(t: f64, rho: f64) -> f64 { 0.0f64.max(1.0 - rho) }
106//!
107//! // define the model, using the sampled or integrated interaction
108//! let sampl_inter = SampledInteraction::new(Wprime);
109//! let integ_inter = SampledInteraction::new(W);
110//! let model = SingleSpeciesModel::new(V, sampl_inter, v);
111//!
112//! // initial condition
113//! const N: usize = 201;
114//! let x0 = SVector::<f64, N>::from_iterator((0..N).map(|i| 3.0 * i as f64 / (N - 1) as f64 - 1.5));
115//!
116//! // configure the solver
117//! let t_start = 0.0;
118//! let t_end = 15.0;
119//! let dt = 0.001;
120//! let rtol = 1e-10;
121//! let atol = 1e-10;
122//! let mut solver = Dopri5::new(model, t_start, t_end, dt, x0, rtol, atol);
123//!
124//! // solve the system of ODEs
125//! solver.integrate();
126//! let t = solver.x_out(); // times
127//! let x = solver.y_out(); // particles positions
128//! let n_steps = t.len();
129//! ```
130//! Plotting (some of) the trajectories with [`plotters`](https://crates.io/crates/plotters)
131//! produces the following picture:
132//!
133//! ![Traffic with `ode_solver` feature][traffic_ode_solver]
134//!
135#![doc = ::embed_doc_image::embed_image!("traffic_ode_solver", "doc/imgs/traffic_ode_solver.png")]
136#![cfg_attr(docsrs, feature(doc_cfg))]
137
138/// A time-dependent velocity field that affects the particles.
139pub trait ExternalVelocity<T, X = T> {
140 fn eval(&self, t: T, x: X) -> X;
141}
142
143/// A time-dependent interaction between the particles.
144pub trait Interaction<T, X = T> {
145 fn eval<P>(&self, t: T, x: X, p: P) -> X
146 where
147 P: IntoIterator<Item = X>;
148}
149
150/// A time-dependent mobility.
151pub trait Mobility<T, X = T> {
152 fn eval(&self, t: T, x: X) -> X;
153}
154
155mod _zero;
156pub use _zero::{OneMobility, ZeroInteraction, ZeroVelocity};
157
158pub struct Velocity<V>(V);
159
160impl<V> Velocity<V> {
161 #[allow(non_snake_case)]
162 pub fn new(V: V) -> Self {
163 Self(V)
164 }
165}
166
167impl<T, X, V> ExternalVelocity<T, X> for Velocity<V>
168where
169 V: Fn(T, X) -> X,
170{
171 #[inline]
172 fn eval(&self, t: T, x: X) -> X {
173 self.0(t, x)
174 }
175}
176
177impl<T, X, V> ExternalVelocity<T, X> for V
178where
179 V: Fn(T, X) -> X,
180{
181 #[inline]
182 fn eval(&self, t: T, x: X) -> X {
183 self(t, x)
184 }
185}
186
187impl<T, X, F> Mobility<T, X> for F
188where
189 F: Fn(T, X) -> X,
190{
191 #[inline]
192 fn eval(&self, t: T, x: X) -> X {
193 self(t, x)
194 }
195}
196
197pub struct SampledInteraction<Wprime>(Wprime);
198
199impl<Wprime> SampledInteraction<Wprime> {
200 #[allow(non_snake_case)]
201 pub fn new(Wprime: Wprime) -> Self {
202 Self(Wprime)
203 }
204}
205
206impl<T, X, Wprime> Interaction<T, X> for SampledInteraction<Wprime>
207where
208 T: Copy,
209 X: Copy + num_traits::real::Real,
210 Wprime: Fn(T, X) -> X,
211{
212 fn eval<P>(&self, t: T, x: X, p: P) -> X
213 where
214 P: IntoIterator<Item = X>,
215 {
216 let mut sum = X::zero();
217 let mut len = 0;
218 for y in p.into_iter() {
219 len += 1;
220 let r = x - y;
221 // The function `self.0` (which is `W'`) must return `0` if `y == x`.
222 // We can ensure it here by skipping the `x` when we encounter it.
223 if r != X::zero() {
224 sum = sum + self.0(t, r);
225 }
226 }
227 -X::from(sum).unwrap() / X::from(len - 1).unwrap()
228 }
229}
230
231pub struct IntegratedInteraction<W>(W);
232
233impl<W> IntegratedInteraction<W> {
234 #[allow(non_snake_case)]
235 pub fn new(W: W) -> Self {
236 Self(W)
237 }
238}
239
240impl<T, X, W> Interaction<T, X> for IntegratedInteraction<W>
241where
242 T: Copy,
243 X: Copy + num_traits::real::Real,
244 W: Fn(T, X) -> X,
245{
246 fn eval<P>(&self, t: T, x: X, p: P) -> X
247 where
248 P: IntoIterator<Item = X>,
249 {
250 let mut p = p.into_iter().peekable();
251 let mut total = X::zero();
252 let mut left_dens = X::zero();
253 let mut len = 0;
254 while let Some(y) = p.next() {
255 len += 1;
256 let right_dens = if let Some(z) = p.peek() {
257 (*z - y).recip()
258 } else {
259 X::zero()
260 };
261 total = total + self.0(t, x - y) * (right_dens - left_dens);
262 left_dens = right_dens;
263 }
264 -total / X::from(len - 1).unwrap()
265 }
266}
267
268pub struct SingleSpeciesModel<T, X, Vel, Int, Mob> {
269 pub vel: Vel,
270 pub int: Int,
271 pub mob: Mob,
272 _t: std::marker::PhantomData<T>,
273 _x: std::marker::PhantomData<X>,
274}
275
276impl<T, X, Vel, Int, Mob> SingleSpeciesModel<T, X, Vel, Int, Mob>
277where
278 Vel: ExternalVelocity<T, X>,
279 Int: Interaction<T, X>,
280 Mob: Mobility<T, X>,
281{
282 pub fn new(vel: Vel, int: Int, mob: Mob) -> Self {
283 Self {
284 vel,
285 int,
286 mob,
287 _t: std::marker::PhantomData,
288 _x: std::marker::PhantomData,
289 }
290 }
291}
292
293#[cfg(feature = "ode_solver")]
294pub use _ode_solver::*;
295
296#[cfg(feature = "ode_solver")]
297#[cfg_attr(docsrs, doc(cfg(feature = "ode_solver")))]
298mod _ode_solver {
299 use super::*;
300
301 use ode_solvers::{SVector, System};
302
303 impl<X, Vel, Int, Mob, const N: usize> System<SVector<X, N>>
304 for SingleSpeciesModel<f64, X, Vel, Int, Mob>
305 where
306 Vel: ExternalVelocity<f64, X>,
307 Int: Interaction<f64, X>,
308 Mob: Mobility<f64, X>,
309 X: num_traits::real::Real + nalgebra::base::Scalar,
310 {
311 fn system(&self, t: f64, x: &SVector<X, N>, dx: &mut SVector<X, N>) {
312 let n = x.len();
313 let f = X::from(n - 1).unwrap().recip();
314 for i in 0..n {
315 let vel = self.vel.eval(t, x[i]);
316 let int = self.int.eval(t, x[i], x.iter().cloned());
317 let tot = vel + int;
318 let dens = if tot.is_sign_positive() {
319 if i < n - 1 {
320 f / (x[i + 1] - x[i])
321 } else {
322 X::zero()
323 }
324 } else {
325 if i > 0 {
326 f / (x[i] - x[i - 1])
327 } else {
328 X::zero()
329 }
330 };
331 let m = self.mob.eval(t, dens);
332 dx[i] = tot * m;
333 }
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn zero_velocity() {
344 assert_eq!(ZeroVelocity.eval(0.0, 0.0), 0.0);
345 }
346
347 #[test]
348 fn zero_interaction_potential() {
349 let p = [1., 2., 3.];
350 assert_eq!(ZeroInteraction.eval(0.0, 0.0, p), 0.0);
351 }
352
353 #[test]
354 fn velocity() {
355 let vel = Velocity(|t: f64, x: f64| t + x);
356 assert_eq!(vel.eval(2., 3.), 5.);
357 }
358
359 #[test]
360 fn sampled_interaction() {
361 let int = SampledInteraction(|_t: f64, x: f64| x);
362 let p = [0., 1., 2., 3.];
363 assert_eq!(int.eval(0., 0., p), (0. + 1. + 2. + 3.) / (4. - 1.));
364 }
365
366 #[test]
367 fn integrated_interaction() {
368 let int = IntegratedInteraction(|_t: f64, x: f64| x * x / 2.);
369 let p = [0., 1., 2., 3.];
370 assert_eq!(int.eval(0., 0., p), (3. * 3. / 2. - 0.) / 3.);
371 }
372}