Skip to content

Commit d1e0d22

Browse files
authored
Merge pull request #848 from DaddyWesker/random_seed
set-random-seed added
2 parents a6f99a2 + c2c1b8b commit d1e0d22

File tree

4 files changed

+254
-32
lines changed

4 files changed

+254
-32
lines changed

lib/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ env_logger = { workspace = true }
1010
directories = "5.0.1" # For Environment to find platform-specific config location
1111
smallvec = "1.10.0"
1212
im = "15.1.0"
13-
rand = "0.8.5"
13+
rand = "0.9.0"
1414
bitset = "0.1.2"
1515
dyn-fmt = "0.4.0"
1616
itertools = "0.13.0"

lib/src/metta/runner/stdlib/random.rs

Lines changed: 228 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,79 @@
1+
12
use crate::*;
23
use crate::metta::*;
34
use super::{grounded_op, regex};
45
use crate::metta::text::Tokenizer;
56
use crate::metta::runner::number::*;
67
use crate::metta::runner::bool::*;
78

8-
use std::fmt::Display;
9-
use rand::Rng;
9+
use std::fmt::{Display, Formatter};
10+
use std::cell::RefCell;
11+
use rand::{Rng, SeedableRng, rngs::StdRng};
12+
use std::rc::Rc;
1013

1114
//TODO: In the current version of rand it is possible for rust to hang if range end's value is too
1215
// big. In future releases (0.9+) of rand signature of sample_single will be changed and it will be
1316
// possible to use match construction to cover overflow and other errors. So after library will be
1417
// upgraded RandomInt and RandomFloat codes should be altered.
1518
// see comment https://github.com/trueagi-io/hyperon-experimental/pull/791#discussion_r1824355414
19+
20+
pub const ATOM_TYPE_RANDOM_GENERATOR : Atom = sym!("RandomGenerator");
21+
22+
#[derive(Clone, Debug)]
23+
pub struct RandomGenerator(Rc<RefCell<StdRng>>);
24+
25+
impl RandomGenerator {
26+
fn from_os_rng() -> Self {
27+
Self(Rc::new(RefCell::new(StdRng::from_os_rng())))
28+
}
29+
30+
fn from_seed_u64(seed: u64) -> Self {
31+
Self(Rc::new(RefCell::new(StdRng::seed_from_u64(seed))))
32+
}
33+
34+
fn reseed_from_u64(&self, seed: u64) {
35+
*self.0.borrow_mut() = StdRng::seed_from_u64(seed);
36+
}
37+
38+
fn reset(&self) {
39+
*self.0.borrow_mut() = StdRng::from_os_rng();
40+
}
41+
42+
fn random_range<T, R>(&self, range: R) -> T
43+
where
44+
T: rand::distr::uniform::SampleUniform,
45+
R: rand::distr::uniform::SampleRange<T>,
46+
{
47+
self.0.borrow_mut().random_range(range)
48+
}
49+
}
50+
51+
impl Grounded for RandomGenerator {
52+
fn type_(&self) -> Atom {
53+
ATOM_TYPE_RANDOM_GENERATOR
54+
}
55+
}
56+
57+
impl Display for RandomGenerator {
58+
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
59+
write!(f, "RandomGenerator-{:?}", self.0.as_ptr())
60+
}
61+
}
62+
63+
impl PartialEq for RandomGenerator {
64+
fn eq(&self, other: &Self) -> bool {
65+
self.0.as_ptr() == other.0.as_ptr()
66+
}
67+
}
68+
1669
#[derive(Clone, Debug)]
1770
pub struct RandomIntOp {}
1871

1972
grounded_op!(RandomIntOp, "random-int");
2073

2174
impl Grounded for RandomIntOp {
2275
fn type_(&self) -> Atom {
23-
Atom::expr([ARROW_SYMBOL, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER])
76+
Atom::expr([ARROW_SYMBOL, ATOM_TYPE_RANDOM_GENERATOR, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER])
2477
}
2578

2679
fn as_execute(&self) -> Option<&dyn CustomExecute> {
@@ -30,15 +83,16 @@ impl Grounded for RandomIntOp {
3083

3184
impl CustomExecute for RandomIntOp {
3285
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
33-
let arg_error = || ExecError::from("random-int expects two arguments: number (start) and number (end)");
34-
let start: i64 = args.get(0).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
35-
let end: i64 = args.get(1).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
86+
let arg_error = || ExecError::from("random-int expects three arguments: random generator, number (start) and number (end)");
87+
let generator = args.get(0).ok_or_else(arg_error)?.into();
88+
let start: i64 = args.get(1).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
89+
let end: i64 = args.get(2).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
90+
let generator = Atom::as_gnd::<RandomGenerator>(generator).ok_or("random-int expects a random generator as its argument")?;
3691
let range = start..end;
3792
if range.is_empty() {
38-
return Err(ExecError::from("Range is empty"));
93+
return Err(ExecError::from("RangeIsEmpty"));
3994
}
40-
let mut rng = rand::thread_rng();
41-
Ok(vec![Atom::gnd(Number::Integer(rng.gen_range(range)))])
95+
Ok(vec![Atom::gnd(Number::Integer(generator.random_range(range)))])
4296
}
4397
}
4498

@@ -49,7 +103,7 @@ grounded_op!(RandomFloatOp, "random-float");
49103

50104
impl Grounded for RandomFloatOp {
51105
fn type_(&self) -> Atom {
52-
Atom::expr([ARROW_SYMBOL, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER])
106+
Atom::expr([ARROW_SYMBOL, ATOM_TYPE_RANDOM_GENERATOR, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER])
53107
}
54108

55109
fn as_execute(&self) -> Option<&dyn CustomExecute> {
@@ -59,24 +113,101 @@ impl Grounded for RandomFloatOp {
59113

60114
impl CustomExecute for RandomFloatOp {
61115
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
62-
let arg_error = || ExecError::from("random-float expects two arguments: number (start) and number (end)");
63-
let start: f64 = args.get(0).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
64-
let end: f64 = args.get(1).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
116+
let arg_error = || ExecError::from("random-float expects three arguments: random generator, number (start) and number (end)");
117+
let generator = args.get(0).ok_or_else(arg_error)?.into();
118+
let start: f64 = args.get(1).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
119+
let end: f64 = args.get(2).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
120+
let generator = Atom::as_gnd::<RandomGenerator>(generator).ok_or("random-float expects a random generator as its argument")?;
65121
let range = start..end;
66122
if range.is_empty() {
67-
return Err(ExecError::from("Range is empty"));
123+
return Err(ExecError::from("RangeIsEmpty"));
68124
}
69-
let mut rng = rand::thread_rng();
70-
Ok(vec![Atom::gnd(Number::Float(rng.gen_range(range)))])
125+
Ok(vec![Atom::gnd(Number::Float(generator.random_range(range)))])
126+
}
127+
}
128+
129+
#[derive(Clone, Debug)]
130+
pub struct SetRandomSeedOp {}
131+
132+
grounded_op!(SetRandomSeedOp, "set-random-seed");
133+
134+
impl Grounded for SetRandomSeedOp {
135+
fn type_(&self) -> Atom {
136+
Atom::expr([ARROW_SYMBOL, ATOM_TYPE_RANDOM_GENERATOR, ATOM_TYPE_NUMBER, UNIT_TYPE])
137+
}
138+
139+
fn as_execute(&self) -> Option<&dyn CustomExecute> {
140+
Some(self)
71141
}
72142
}
73143

144+
impl CustomExecute for SetRandomSeedOp {
145+
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
146+
let arg_error = || ExecError::from("set-random-seed expects two arguments: random generator and number (seed)");
147+
let generator = args.get(0).ok_or_else(arg_error)?.into();
148+
let seed: i64 = args.get(1).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
149+
let generator = Atom::as_gnd::<RandomGenerator>(generator).ok_or("set-random-seed expects a random generator as its argument")?;
150+
generator.reseed_from_u64(seed as u64);
151+
Ok(vec![UNIT_ATOM])
152+
}
153+
}
154+
155+
#[derive(Clone, Debug)]
156+
pub struct NewRandomGeneratorOp {}
157+
158+
grounded_op!(NewRandomGeneratorOp, "new-random-generator");
159+
160+
impl Grounded for NewRandomGeneratorOp {
161+
fn type_(&self) -> Atom {
162+
Atom::expr([ARROW_SYMBOL, ATOM_TYPE_NUMBER, ATOM_TYPE_RANDOM_GENERATOR])
163+
}
164+
165+
fn as_execute(&self) -> Option<&dyn CustomExecute> {
166+
Some(self)
167+
}
168+
}
169+
170+
impl CustomExecute for NewRandomGeneratorOp {
171+
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
172+
let arg_error = || ExecError::from("new-random-generator expects one argument: number (seed)");
173+
let seed: i64 = args.get(0).and_then(Number::from_atom).ok_or_else(arg_error)?.into();
174+
let new_generator = RandomGenerator::from_seed_u64(seed as u64);
175+
Ok(vec![Atom::gnd(new_generator)])
176+
}
177+
}
178+
179+
#[derive(Clone, Debug)]
180+
pub struct ResetRandomGeneratorOp {}
181+
182+
grounded_op!(ResetRandomGeneratorOp, "reset-random-generator");
183+
184+
impl Grounded for ResetRandomGeneratorOp {
185+
fn type_(&self) -> Atom {
186+
Atom::expr([ARROW_SYMBOL, ATOM_TYPE_RANDOM_GENERATOR, UNIT_ATOM])
187+
}
188+
189+
fn as_execute(&self) -> Option<&dyn CustomExecute> {
190+
Some(self)
191+
}
192+
}
193+
194+
impl CustomExecute for ResetRandomGeneratorOp {
195+
fn execute(&self, args: &[Atom]) -> Result<Vec<Atom>, ExecError> {
196+
let arg_error = || ExecError::from("reset-random-generator expects one argument: random generator");
197+
let generator = args.get(0).ok_or_else(arg_error)?.into();
198+
let generator = Atom::as_gnd::<RandomGenerator>(generator).ok_or("set-random-seed expects a random generator as its argument")?;
199+
generator.reset();
200+
Ok(vec![UNIT_ATOM])
201+
}
202+
}
203+
204+
74205
// NOTE: flip is absent in Python intentionally for conversion testing
75206
#[derive(Clone, PartialEq, Debug)]
76207
pub struct FlipOp{}
77208

78209
impl Display for FlipOp {
79-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80211
write!(f, "flip")
81212
}
82213
}
@@ -102,6 +233,14 @@ pub fn register_common_tokens(tref: &mut Tokenizer) {
102233
tref.register_token(regex(r"random-int"), move |_| { random_int_op.clone() });
103234
let random_float_op = Atom::gnd(RandomFloatOp{});
104235
tref.register_token(regex(r"random-float"), move |_| { random_float_op.clone() });
236+
let set_seed_op = Atom::gnd(SetRandomSeedOp{});
237+
tref.register_token(regex(r"set-random-seed"), move |_| { set_seed_op.clone() });
238+
let new_random_generator_op = Atom::gnd(NewRandomGeneratorOp{});
239+
tref.register_token(regex(r"new-random-generator"), move |_| { new_random_generator_op.clone() });
240+
let reset_random_generator_op = Atom::gnd(ResetRandomGeneratorOp{});
241+
tref.register_token(regex(r"reset-random-generator"), move |_| { reset_random_generator_op.clone() });
242+
let generator = RandomGenerator::from_os_rng();
243+
tref.register_token(regex(r"&rng"), move |_| { Atom::gnd(generator.clone()) });
105244
let flip_op = Atom::gnd(FlipOp{});
106245
tref.register_token(regex(r"flip"), move |_| { flip_op.clone() });
107246
}
@@ -113,26 +252,88 @@ mod tests {
113252

114253
#[test]
115254
fn metta_random() {
116-
assert_eq!(run_program(&format!("!(chain (eval (random-int 0 5)) $rint (and (>= $rint 0) (< $rint 5)))")), Ok(vec![vec![expr!({Bool(true)})]]));
117-
assert_eq!(run_program(&format!("!(random-int 0 0)")), Ok(vec![vec![expr!("Error" ({ RandomIntOp{} } {Number::Integer(0)} {Number::Integer(0)}) "Range is empty")]]));
118-
assert_eq!(run_program(&format!("!(chain (eval (random-float 0.0 5.0)) $rfloat (and (>= $rfloat 0.0) (< $rfloat 5.0)))")), Ok(vec![vec![expr!({Bool(true)})]]));
119-
assert_eq!(run_program(&format!("!(random-float 0 -5)")), Ok(vec![vec![expr!("Error" ({ RandomFloatOp{} } {Number::Integer(0)} {Number::Integer(-5)}) "Range is empty")]]));
255+
assert_eq!(run_program(&format!(
256+
"!(chain (eval (random-int &rng 0 5)) $rint
257+
(and (>= $rint 0) (< $rint 5)))")),
258+
Ok(vec![vec![expr!({Bool(true)})]]));
259+
assert_eq!(run_program(&format!(
260+
"!(assertEqual
261+
(random-int &rng 5 0)
262+
(Error (random-int &rng 5 0) RangeIsEmpty))")),
263+
Ok(vec![vec![UNIT_ATOM]]));
264+
assert_eq!(run_program(&format!(
265+
"!(chain (eval (random-float &rng 0.0 5.0)) $rfloat
266+
(and (>= $rfloat 0.0) (< $rfloat 5.0)))")),
267+
Ok(vec![vec![expr!({Bool(true)})]]));
268+
assert_eq!(run_program(&format!(
269+
"!(assertEqual
270+
(random-float &rng 5 0)
271+
(Error (random-float &rng 5 0) RangeIsEmpty))")),
272+
Ok(vec![vec![UNIT_ATOM]]));
273+
274+
assert_eq!(run_program(&format!("!(set-random-seed &rng 0)")), Ok(vec![vec![UNIT_ATOM]]));
275+
276+
assert_eq!(run_program(&format!("
277+
!(bind! &newrng (new-random-generator 0))
278+
!(random-float &newrng 0 10)
279+
")), run_program(&format!("
280+
!(bind! &newrng (new-random-generator 0))
281+
!(random-float &newrng 0 10)
282+
")));
283+
284+
assert_eq!(run_program(&format!(
285+
"!(let $newrng (new-random-generator 0)
286+
(let $t (set-random-seed $newrng 5)
287+
(let $res_1 (random-float $newrng 0 5)
288+
(let $t2 (set-random-seed $newrng 5)
289+
(let $res_2 (random-float $newrng 0 5)
290+
(== $res_1 $res_2))))))"
291+
)), Ok(vec![vec![expr!({Bool(true)})]]));
292+
293+
assert_eq!(run_program(&format!(
294+
"!(let $seededrng (new-random-generator 0)
295+
(let $seededrng2 (new-random-generator 0)
296+
(let $t (reset-random-generator $seededrng)
297+
(let $rfloat (random-float $seededrng 0 100)
298+
(let $rfloat2 (random-float $seededrng2 0 100)
299+
(== $rfloat $rfloat2))))) )"
300+
)), Ok(vec![vec![expr!({Bool(false)})]]));
301+
302+
assert_eq!(run_program(&format!("
303+
!(let $newrng (new-random-generator 0)
304+
(let $t (reset-random-generator $newrng)
305+
(let $res (random-float $newrng 0 5)
306+
(and (>= $res 0.0) (< $res 5.0)))))")),
307+
Ok(vec![vec![expr!({Bool(true)})]]));
120308
}
121309

122310
#[test]
123311
fn random_op() {
124-
let res = RandomIntOp{}.execute(&mut vec![expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]);
312+
let res = RandomIntOp{}.execute(&mut vec![expr!({RandomGenerator::from_os_rng()}), expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]);
125313
let range = 0..5;
126314
let res_i64: i64 = res.unwrap().get(0).and_then(Number::from_atom).unwrap().into();
127315
assert!(range.contains(&res_i64));
128-
let res = RandomIntOp{}.execute(&mut vec![expr!({Number::Integer(2)}), expr!({Number::Integer(-2)})]);
129-
assert_eq!(res, Err(ExecError::from("Range is empty")));
316+
let res = RandomIntOp{}.execute(&mut vec![expr!({RandomGenerator::from_os_rng()}), expr!({Number::Integer(2)}), expr!({Number::Integer(-2)})]);
317+
assert_eq!(res, Err(ExecError::from("RangeIsEmpty")));
130318

131-
let res = RandomFloatOp{}.execute(&mut vec![expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]);
319+
let res = RandomFloatOp{}.execute(&mut vec![expr!({RandomGenerator::from_os_rng()}), expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]);
132320
let range = 0.0..5.0;
133321
let res_f64: f64 = res.unwrap().get(0).and_then(Number::from_atom).unwrap().into();
134322
assert!(range.contains(&res_f64));
135-
let res = RandomFloatOp{}.execute(&mut vec![expr!({Number::Integer(0)}), expr!({Number::Integer(0)})]);
136-
assert_eq!(res, Err(ExecError::from("Range is empty")));
323+
let res = RandomFloatOp{}.execute(&mut vec![expr!({RandomGenerator::from_os_rng()}), expr!({Number::Integer(0)}), expr!({Number::Integer(0)})]);
324+
assert_eq!(res, Err(ExecError::from("RangeIsEmpty")));
325+
326+
let gen = NewRandomGeneratorOp{}.execute(&mut vec![expr!({Number::Integer(0)})]);
327+
let res1 = RandomFloatOp{}.execute(&mut vec![expr!({gen}), expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]);
328+
let gen = NewRandomGeneratorOp{}.execute(&mut vec![expr!({Number::Integer(0)})]);
329+
let res2 = RandomFloatOp{}.execute(&mut vec![expr!({gen}), expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]);
330+
assert_eq!(res1, res2);
331+
332+
let gen = NewRandomGeneratorOp{}.execute(&mut vec![expr!({Number::Integer(0)})]);
333+
let _ = SetRandomSeedOp{}.execute(&mut vec![expr!({gen.clone()}), expr!({Number::Integer(0)})]);
334+
let res1 = RandomFloatOp{}.execute(&mut vec![expr!({gen.clone()}), expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]);
335+
let _ = SetRandomSeedOp{}.execute(&mut vec![expr!({gen.clone()}), expr!({Number::Integer(5)})]);
336+
let res2 = RandomFloatOp{}.execute(&mut vec![expr!({gen.clone()}), expr!({Number::Integer(0)}), expr!({Number::Integer(5)})]);
337+
assert_eq!(res1, res2);
137338
}
138339
}

lib/src/metta/runner/stdlib/stdlib.metta

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,40 @@
204204
(@return "True/False"))
205205

206206
(@doc random-int
207-
(@desc "Returns random int number from range defined by two numbers (first and second argument)")
207+
(@desc "Returns random int number from range defined by two numbers (second and third argument)")
208208
(@params (
209+
(@param "Random number generator instance")
209210
(@param "Range start")
210211
(@param "Range end")))
211212
(@return "Random int number from defined range"))
212213

213214
(@doc random-float
214-
(@desc "Returns random float number from range defined by two numbers (first and second argument)")
215+
(@desc "Returns random float number from range defined by two numbers (second and third argument)")
215216
(@params (
217+
(@param "Random number generator instance")
216218
(@param "Range start")
217219
(@param "Range end")))
218220
(@return "Random float number from defined range"))
219221

222+
(@doc set-random-seed
223+
(@desc "Sets a new seed (second argument) for random number generator (first argument)")
224+
(@params (
225+
(@param "Random number generator instance")
226+
(@param "Seed")))
227+
(@return "Unit atom"))
228+
229+
(@doc new-random-generator
230+
(@desc "Creates new random number generator instance using seed as input (first argument)")
231+
(@params (
232+
(@param "Seed")))
233+
(@return "Instance of random number generator"))
234+
235+
(@doc reset-random-generator
236+
(@desc "Resets instance of random number generator (first argument) to its default behavior (StdRng::from_os_rng())")
237+
(@params (
238+
(@param "Random number generator instance")))
239+
(@return "Random number generator instance with default behavior"))
240+
220241
(@doc collapse-bind
221242
(@desc "Evaluates minimal MeTTa operation (first argument) and returns an expression which contains all alternative evaluations in a form (Atom Bindings). Bindings are represented in a form of a grounded atom.")
222243
(@params (

0 commit comments

Comments
 (0)