Skip to content

Commit 6e1154d

Browse files
committed
added params to integral macro, start on numeric integration
1 parent 76d1dc4 commit 6e1154d

7 files changed

+152
-46
lines changed

reactive/integral-preprocessor.rkt

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#lang s-exp rosette
2+
3+
(provide integral-preprocessor symbolic-integral)
4+
5+
; The integral-preprocessor function takes the expression being integrated, e.g. (integral expr)
6+
; plus additional arguments, representing keyword arguments in the source.
7+
; var is the variable of integration - this has a default value of (milliseconds), provided by the caller though
8+
; numeric-kw is #t if the keyword #:numeric was provided
9+
; symbolic-kw is #t if the keyword #:symbolic was provided
10+
; dt is the time step if the keyword #:dt and an argument was provided
11+
; Only one of #:numeric or #:symbolic can be given -- a Racket macro ninja would check this in the macro, but
12+
; here this function checks it. If neither the code first tries to find a symbolic integral, and if that doesn't
13+
; work, it returns code to compute it numerically. #:dt can only be provided if #:numeric is specified. Again,
14+
; a macro ninja would check this in the macro rather than here.
15+
; It returns 3 values:
16+
; symbolic? #t if the result is a symbolic integration, #f if numeric
17+
; expr if symbolic is #t, the symbolic integral; and otherwise null
18+
; dt if symbolic is #f, the time step for numeric integration; and otherwise null
19+
(define (integral-preprocessor expr var numeric-kw symbolic-kw dt)
20+
(when (and numeric-kw symbolic-kw) (error "can only specify one of #:numeric and #:symbolic"))
21+
(when (and (not numeric-kw) dt) (error "#:dt can only be specified in conjunction with #:numeric"))
22+
; try to compute the symbolic integral and bind it to s, unless #:numeric is specified
23+
(let ([s (if numeric-kw #f (symbolic-integral expr var))])
24+
(when (and symbolic-kw (not s)) (error "#:symbolic was specified but unable to find symbolic integral"))
25+
(if symbolic-integral (values #t s #f) (values #f (null) dt))))
26+
27+
; Function to do symbolic integration at compile time -- super simple to start with.
28+
; This doesn't do any simplification of the result -- which seems fine, since it is for evaluation
29+
; rather than human consumption, and the code is going to be really fast with or without simplication.
30+
; It is small, but putting it in a separate module makes it easier to test independently.
31+
(define (symbolic-integral expr var)
32+
(match expr
33+
[v #:when (equal? v var) (list '* 0.5 (list 'expt var 2))]
34+
[(? number? n) (list '* n var)]
35+
[(list '+ x y) (list '+ (symbolic-integral x var) (symbolic-integral y var))]
36+
[(cons '* (list-no-order (? number? n) x)) (list '* n (symbolic-integral x var))]
37+
[(list '/ x (? number? n)) (list '/ (symbolic-integral x var) n)]
38+
[(list 'expt v (? number? n)) #:when (equal? v var) (list '/ (list 'expt v (+ n 1)) (+ n 1))]
39+
[(list 'integral e) (symbolic-integral (symbolic-integral e var) var)]
40+
[_ #f]))

reactive/reactive-thing.rkt

+28-9
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,36 @@
9090
[(_ fn expr)
9191
(with-syntax ([id (datum->syntax stx (gensym))])
9292
#'(send this max-min-helper fn (lambda () (send this wally-evaluate expr)) 'id (interesting-time?)))]))
93+
9394
; integral macro
9495
; The form is (integral expr) with additional optional keyword arguments as follows:
9596
; #:var v -- variable of integration; default is (milliseconds) -- note that an expression is allowed here
96-
; #:numeric or #:symbolic -- which kind of integration to use. Default is to try symbolic, and if that doesn't work, use numeric
97+
; #:numeric or #:symbolic -- which kind of integration to use. Can provide at most one of these, or omit.
98+
; The default is to try symbolic, and if that doesn't work, use numeric. However, if #:symbolic is listed
99+
; explicitly, then either symbolic integration must succeed or the system raises an exception.
97100
; #:dt d -- time step (only allowed with #:numeric)
98101
; Example: (integral (sin x) #:var x #:numeric #:dt 0.1)
99-
(require (for-syntax "symbolic-integration.rkt")) ; function to do simple symbolic integration
102+
(require (for-syntax "integral-preprocessor.rkt"))
100103
(require (for-syntax syntax/parse))
101104
(define-syntax (integral stx)
102105
(syntax-parse stx
103-
[(integral e:expr (~or (~optional (~seq #:var v:expr)) (~optional #:numeric) (~optional #:symbolic) (~optional (~seq #:dt d:expr))) ...)
104-
(with-syntax ([intgl (datum->syntax stx (symbolic-integral (syntax->datum #'e)
105-
(if (attribute v) (syntax->datum #'v) '(milliseconds))))]
106-
[id (datum->syntax stx (gensym))])
107-
#'(send this integral-helper (lambda () (send this wally-evaluate intgl)) 'id (interesting-time?)))]))
106+
[(integral e:expr (~or (~optional (~seq #:var v:expr))
107+
(~optional (~seq (~and #:numeric numeric-kw)))
108+
(~optional (~seq (~and #:symbolic symbolic-kw)))
109+
(~optional (~seq #:dt dt:expr))) ...)
110+
(let-values ([(symbolic? symbolic-integral dt)
111+
(integral-preprocessor (syntax->datum #'e)
112+
(if (attribute v) (syntax->datum #'v) '(milliseconds))
113+
(attribute numeric-kw)
114+
(attribute symbolic-kw)
115+
(if (attribute dt) (syntax->datum #'dt) #f))])
116+
(if symbolic?
117+
(with-syntax ([s (datum->syntax stx symbolic-integral)] ; symbolic version
118+
[id (datum->syntax stx (gensym))])
119+
#'(send this integral-symbolic-run-time-fn (lambda () (send this wally-evaluate s)) 'id (interesting-time?)))
120+
(with-syntax ([d (datum->syntax stx dt)]
121+
[id (datum->syntax stx (gensym))]) ; numeric version
122+
#'(send this integral-numeric-run-time-fn (lambda () (send this wally-evaluate e)) 'id d))))]))
108123

109124

110125
(define reactive-thing%
@@ -146,15 +161,19 @@
146161
(max-or-min (f) (hash-ref accumulator-values id)))])
147162
(hash-set! accumulator-values id updated-value)
148163
updated-value))
149-
; helper for code generated by integral macro
150-
(define/public (integral-helper f id interesting)
164+
; runtime function for code generated by integral macro for symbolic integration -- this should evaluate to the
165+
; correct value for (integral expr) at the current time
166+
(define/public (integral-symbolic-run-time-fn f id interesting)
151167
; If the value for this integral isn't stored already, or if this is the first time through a 'while', save that value.
152168
; Hack (hopefully temporary): if time=0, also save the value (i.e., reset the value for this integral). This is
153169
; to get around a bug where the integral expression is being evaluated on startup before all variables have values.
154170
(cond [(or (not (hash-has-key? accumulator-values id)) (eq? interesting 'first) (equal? (send this milliseconds-evaluated) 0))
155171
(hash-set! accumulator-values id (f))
156172
0.0]
157173
[else (- (f) (hash-ref accumulator-values id))]))
174+
; version for code for numeric integration
175+
(define/public (integral-numeric-run-time-fn f id dt)
176+
(error "not finished\n"))
158177

159178
(define/override (milliseconds)
160179
symbolic-time)

reactive/symbolic-integration.rkt

-19
This file was deleted.

tests/all-tests.rkt

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
"when-tests.rkt"
1616
"while-tests.rkt"
1717
"max-min-tests.rkt"
18-
"symbolic-integration-tests.rkt"
19-
"integral-tests.rkt"
18+
"integral-preprocessor-tests.rkt"
19+
"integral-symbolic-tests.rkt"
20+
"integral-bad-arg-tests.rkt"
2021
"integrals-and-events.rkt"
2122
"viewer-tests.rkt"
2223
"compiled-reactive-thing-tests.rkt"

tests/integral-bad-arg-tests.rkt

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#lang s-exp rosette
2+
;; unit tests for bad argument combinations for integral in reactive-thing%
3+
4+
(require rackunit rackunit/text-ui rosette/lib/roseunit)
5+
(require syntax/macro-testing)
6+
(require "../core/wallingford.rkt")
7+
(require "../reactive/reactive.rkt")
8+
9+
(provide integral-bad-arg-tests)
10+
11+
(define (both-symbolic-and-numeric)
12+
(test-case
13+
"test call to integral with both #:symbolic and #:numeric"
14+
(define tester%
15+
(class reactive-thing%
16+
(inherit milliseconds)
17+
(super-new)
18+
(define-public-symbolic* x real?)
19+
(convert-compile-time-error (always (equal? x (integral 2 #:symbolic #:numeric))))))
20+
(check-exn #rx"can only specify one of #:numeric and #:symbolic" (lambda () (new tester%)))))
21+
22+
(define (dt-with-symbolic)
23+
(test-case
24+
"test call to integral with #:symbolic and #:dt"
25+
(define tester%
26+
(class reactive-thing%
27+
(inherit milliseconds)
28+
(super-new)
29+
(define-public-symbolic* x real?)
30+
(convert-compile-time-error (always (equal? x (integral 2 #:symbolic #:dt 1))))))
31+
(check-exn #rx"#:dt can only be specified in conjunction with #:numeric" (lambda () (new tester%)))))
32+
33+
(define (dt-alone)
34+
(test-case
35+
"test call to integral with #:dt alone (no #:numeric as required)"
36+
(define tester%
37+
(class reactive-thing%
38+
(inherit milliseconds)
39+
(super-new)
40+
(define-public-symbolic* x real?)
41+
(convert-compile-time-error (always (equal? x (integral 2 #:dt 1))))))
42+
(check-exn #rx"#:dt can only be specified in conjunction with #:numeric" (lambda () (new tester%)))))
43+
44+
(define (symbolic-but-too-hard)
45+
(test-case
46+
"test call to integral with #:symbolic and an expression that is too difficult to find the symbolic integral"
47+
(define tester%
48+
(class reactive-thing%
49+
(inherit milliseconds)
50+
(super-new)
51+
(define-public-symbolic* x real?)
52+
(convert-compile-time-error (always (equal? 0 (integral (sin x) #:symbolic))))))
53+
(check-exn #rx"#:symbolic was specified but unable to find symbolic integral" (lambda () (new tester%)))))
54+
55+
56+
(define integral-bad-arg-tests
57+
(test-suite+
58+
"run tests for integral with bad arguments"
59+
(both-symbolic-and-numeric)
60+
(dt-with-symbolic)
61+
(dt-alone)
62+
(symbolic-but-too-hard)
63+
))
64+
65+
(time (run-tests integral-bad-arg-tests))

tests/symbolic-integration-tests.rkt tests/integral-preprocessor-tests.rkt

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#lang s-exp rosette
2-
;; unit tests for symbolic-integration helper function
2+
;; unit tests for integration preprocessor
33

44
(require rackunit rackunit/text-ui rosette/lib/roseunit)
5-
(require "../reactive/symbolic-integration.rkt")
5+
(require "../reactive/integral-preprocessor.rkt")
66

7-
(provide symbolic-integration-tests)
7+
(provide integral-preprocessor-tests)
88

99
; The only tests with various different expressions to integrate are integral-in-always and integral-with-explict-variable-of-integration.
1010
; The other tests just use a constant (because arguably the other functionality being tested is independent of the symbolic integration function).
@@ -39,13 +39,13 @@
3939

4040
(define (too-difficult-expression-test)
4141
(test-case
42-
"expressions that are too difficult"
43-
(check-exn exn:fail? (lambda () (symbolic-integral '(sin x) 'x)))
44-
(check-exn exn:fail? (lambda () (symbolic-integral '(* x x) 'x))) ; needs to use expt
45-
(check-exn exn:fail? (lambda () (symbolic-integral '(/ 2 x) 'x)))
42+
"expressions that are too difficult for the symbolic integrator - these should return #f"
43+
(check-false (symbolic-integral '(sin x) 'x))
44+
(check-false (symbolic-integral '(* x x) 'x)) ; needs to use expt
45+
(check-false (symbolic-integral '(/ 2 x) 'x))
4646
))
4747

48-
(define symbolic-integration-tests
48+
(define integral-preprocessor-tests
4949
(test-suite+
5050
"run tests for symbolic integration function"
5151
(atomic-expression-tests)
@@ -54,4 +54,4 @@
5454
(too-difficult-expression-test)
5555
))
5656

57-
(time (run-tests symbolic-integration-tests))
57+
(time (run-tests integral-preprocessor-tests))

tests/integral-tests.rkt tests/integral-symbolic-tests.rkt

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#lang s-exp rosette
2-
;; unit tests for integral in reactive-thing%
2+
;; unit tests for integral in reactive-thing% using the symbolic option, either explicitly or implicitly
33

44
(require rackunit rackunit/text-ui rosette/lib/roseunit)
55
(require "../core/wallingford.rkt")
66
(require "../applications/geothings.rkt")
77
(require "../reactive/reactive.rkt")
88

9-
(provide integral-tests)
9+
(provide integral-symbolic-tests)
1010

1111
; The only tests with various different expressions to integrate are integral-in-always and integral-with-explict-variable-of-integration.
1212
; The other tests just use a constant (because arguably the other functionality being tested is independent of the symbolic integration function).
@@ -24,7 +24,7 @@
2424
(inherit milliseconds)
2525
(super-new)
2626
(define-public-symbolic* x y real?)
27-
(always (equal? x (integral 2)))
27+
(always (equal? x (integral 2 #:symbolic))) ; do one that explicitly insists that this be symbolic
2828
(always (equal? y (integral (milliseconds))))))
2929
(define r (new tester%))
3030
(send r start)
@@ -122,7 +122,7 @@
122122

123123
(define (integral-with-explict-variable-of-integration)
124124
(test-case
125-
"test call to integral in an always"
125+
"test call to integral with an explicit variable of integration"
126126
(define tester%
127127
(class reactive-thing%
128128
(inherit milliseconds seconds)
@@ -149,14 +149,14 @@
149149
(check equal? (send r get-z2) 20000)))
150150

151151

152-
(define integral-tests
152+
(define integral-symbolic-tests
153153
(test-suite+
154-
"run tests for integral in reactive-thing"
154+
"run tests for integral in reactive-thing% using the symbolic option, either explicitly or implicitly"
155155
(integral-in-always)
156156
(integral-in-simple-while-hit-start)
157157
(integral-in-simple-while-miss-start)
158158
(integral-in-repeating-while)
159159
(integral-with-explict-variable-of-integration)
160160
))
161161

162-
(time (run-tests integral-tests))
162+
(time (run-tests integral-symbolic-tests))

0 commit comments

Comments
 (0)