Skip to content

Commit

Permalink
Merge pull request #55 from scicloj/fix-45-compose-wolf-expressions
Browse files Browse the repository at this point in the history
Generate wolfram.clj with vars for Wolfram symbols
  • Loading branch information
light-matters authored Jun 29, 2024
2 parents 6574c20 + 5860dbc commit 39789bb
Show file tree
Hide file tree
Showing 28 changed files with 6,862 additions and 194 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ temp
*.qmd

dev/*_scratchpad.clj
./symlink-jlink.jar
symlink-jlink.jar
51 changes: 36 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ By linking the two:

* Wolframite lets you **write and evaluate Wolfram/Mathematica code in Clojure** with full **syntactic integration**. Now Clojure programs can take advantage of Wolfram's enormous range of numerical and symbolic mathematics algorithms and fast matrix algebra routines.
* Wolframite provides the **seamless and transparent translation of native data structures** between Clojure and Wolfram. This includes high-precision numbers, matricies, N-dimensional arrays, and evaluated and unevaluated Mathematica expressions and formulae.
* Wolframite lets you **call, pass, and store Wolfram functions just as if they were first-class functions in Clojure.** This is high-level functional programming at its finest. You can write a function in whichever language is more suited to the task and never think again about which platform is evaluating calls to that function.
* Wolframite facilitates **the "Clojurization" of Wolfram's existing parallel-computing capabilities.** Wolfram is not designed for threads or concurrency. It has excellent support for parallel computation, but parallel evaluations are initiated from a single-threaded master kernel which blocks until all parallel evaluations return. By contrast, Wolframite includes a concurrency framework that lets multiple Clojure threads execute Wolfram expressions without blocking others. Now it is easy to run a simulation in Clojure with 1,000 independent threads asynchronously evaluating processor-intensive expressions in Wolfram. The computations will be farmed out adaptively and transparently to however many Wolfram kernels are available on any number of processor cores, either locally or across a cluster, grid, or network.
* Wolframite lets you **write Wolfram as if it was Clojure** by providing Clojure functions and vars for all Wolfram symbols, including docstrings and autocompletion in your favorite IDE
* [Tentative] Wolframite facilitates **the "Clojurization" of Wolfram's existing parallel-computing capabilities.** Wolfram is not designed for threads or concurrency. It has excellent support for parallel computation, but parallel evaluations are initiated from a single-threaded master kernel which blocks until all parallel evaluations return. By contrast, Wolframite includes a concurrency framework that lets multiple Clojure threads execute Wolfram expressions without blocking others. Now it is easy to run a simulation in Clojure with 1,000 independent threads asynchronously evaluating processor-intensive expressions in Wolfram. The computations will be farmed out adaptively and transparently to however many Wolfram kernels are available on any number of processor cores, either locally or across a cluster, grid, or network.
* Notice that you cannot run more Wolfram kernels than your license allows (see `wolframite.runtime.system/kernel-info!`)

Wolframite is open-source and targeted at applications in scientific computing, computational economics, finance, and other fields that rely on the combination of parallelized simulation and high-performance number-crunching. Wolframite gives the programmer access to Clojure's most cutting-edge features--easy concurrency and multithreading, immutable persistent data structures, and software transactional memory---alongside Wolfram's easy-to-use algorithms for numerics, symbolic mathematics, optimization, statistics, visualization, and image-processing.

Expand All @@ -32,9 +33,9 @@ First, if you haven't already, install the [Clojure CLI toolchain](https://cloju

#### Mathematica or Wolfram Engine

Next, obviously, you'll need to ensure that you have Wolfram Engine or Mathematica installed and your license (free for W. E.) registered - make sure you can run these tools on their own before trying Wolframite.
Next, obviously, you'll need to ensure that you have Wolfram Engine or Mathematica installed and your license (free for W. E.) registered - make sure you can run these tools on their own _before_ trying Wolframite.

First of all, you need to initialize a connecting to a Wolfram/Mathematica kernel, like this:
First of all, you need to initialize a connection to a Wolfram/Mathematica kernel, like this:

```clojure
(wolframite.core/init!)
Expand All @@ -54,38 +55,58 @@ export WOLFRAM_INSTALL_PATH=/opt/mathematica/13.1
Start a REPL with Wolframite on the classpath, then initialize it:

```clojure
(require '[wolframite.core :as wl])
(require '[wolframite.core :as wl]
'[wolframite.wolfram :as w]) ; Wolfram symbols as Clojure vars / fns
;; Initialize
(wl/init!) ; => nil
;; Make all Wolfram functions available as symbols in the current ns (takes some seconds!):
(wl/load-all-symbols (symbol (ns-name *ns*)))
;; Use it:
(wl/eval '(Dot [1 2 3] [4 5 6]))
(wl/eval (w/Dot [1 2 3] [4 5 6]))
;=> 32
```

More examples

```clojure
(wl/eval '(D (Power 'x 2) 'x))
(wl/eval (w/D (w/Power 'x 2) 'x))
;=> (* 2 x)
(wl/eval '(ChemicalData "Ethanol" "MolarMass"))
(wl/eval (w/ChemicalData "Ethanol" "MolarMass"))
;=> (Quantity 46.069M (* "Grams" (Power "Moles" -1)))

;; Accessing WlframAlpha
(wl/eval '(WolframAlpha "How many licks does it take to get to the center of a Tootsie Pop?"))
(wl/eval (w/WolframAlpha "How many licks does it take to get to the center of a Tootsie Pop?")) ; BEWARE: must be online
;=> [(-> [["Input" 1] "Plaintext"] "How many licks does it take to get to the Tootsie Roll center of a Tootsie Pop?") (-> [["Result" 1] "Plaintext"] "3481\n(according to student researchers at the University of Cambridge)")]
; FIXME Try this out; when offline, we get just []

(wl/eval '(N Pi 20))
(N 'Pi 20)
(wl/eval (w/N w/Pi 20))
;=> 3.141592653589793238462643383279502884197169399375105820285M

(wl/eval (w/Map (w/fn [x] (w/Sqrt x)) [4 16]))
;=> [2 4]
```

TIP: Cursive - teach it to resolve `w/fn` as `clojure.core/fn`.

NOTE: The `wolframite.wolfram` (`w`) ns has vars for all Wolfram symbols at the time of the last release. Check `w/*wolfram-kernel-name*` for kernel type/version and run `(wolframite.impl.wolfram-syms.write-ns/write-ns!)`
to generate your own wolfram ns with whatever additional symbols your Wolfram/Mathematice has.

#### Learning Wolframite

Read through and play with [explainer.clj](dev%2Fexplainer.clj) and [demo.clj](dev%2Fdemo.clj), which demonstrate most of Wolframite's features and what you can do with Wolfram.

#### Customizing Wolframite

A big advantage of Wolframite (as opposed to its earlier incarnations) is that we can now individually tailor the user experience at the level of initialization,
```clojure
(wl/init! {:aliases '{** Power}})
(wl/eval '(** 2 5)) ; => 32
```
,
and function call,
```clojure
(wl/init!)
(wl/eval '(** 2 5) {:aliases '{** Power}}) ; => 32
```
. Use it how you want to!

### Clerk Integration

Example usage: (watching for changes in a folder)
Expand All @@ -100,7 +121,7 @@ user> (ch/clerk-watch! ["dev/notebook"])

## Dependencies

Wolframite requires Wolfram's Java integration library JLink, which is currently only available with a Wolfram Engine or Mathematica installation. It will also need to know where the `WolframKernel` / `MathKernel` executable is, in order to be able to start the external evaluation kernel process. Normally, `wolframite.jlink` should be able to find these automatically, if you installed either into a standard location on Mac, Linux or Windows. However, if necessary, you can specify either with env variables / sys properties - see Prerequisites above.
Wolframite requires Wolfram's Java integration library JLink, which is currently only available with a Wolfram Engine or Mathematica installation. It will also need to know where the `WolframKernel` / `MathKernel` executable is, in order to be able to start the external evaluation kernel process. Normally, `wl/init!` should be able to find these automatically, if you installed either into a standard location on Mac, Linux or Windows. However, if necessary, you can specify either with env variables / sys properties - see Prerequisites above.

## Development

Expand Down
6 changes: 3 additions & 3 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{:paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.12.0-alpha12"}
:deps {org.clojure/clojure {:mvn/version "1.12.0-beta1"}
org.clojure/tools.logging {:mvn/version "1.3.0"}
org.scicloj/kindly {:mvn/version "4-beta2"}
babashka/fs {:mvn/version "0.5.20"}}

Expand All @@ -17,8 +18,7 @@
org.slf4j/slf4j-simple {:mvn/version "2.0.9"}}}
:test-run ; run with clj -X:test-run
{:extra-paths ["test"]
:extra-deps {wolfram/jlink
{:local/root "./symlink-jlink.jar"} ; set it for your system
:extra-deps {wolfram/jlink {:local/root "./symlink-jlink.jar"} ; set it for your system
io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
:main-opts ["--main" "cognitect.test-runner"]
Expand Down
57 changes: 30 additions & 27 deletions dev/demo.clj
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
(ns demo
"Demonstrate key features of Wolframite.
See also the explainer ns."
;; FIXME Why do we have both demo and explainer? Could we settle on one? If not then clarify the difference!
(:require
[wolframite.core :as wl]
[wolframite.tools.graphics :as graphics]
[wolframite.base.parse :as parse]
[wolframite.runtime.defaults :as defaults]
[wolframite.base.convert :as convert]
[wolframite.base.evaluate :as evaluate]
[wolframite.base.express :as express]))
[wolframite.core :as wl]
[wolframite.impl.jlink-instance :as jlink-instance]
[wolframite.wolfram :as w]
[wolframite.tools.graphics :as graphics]
[wolframite.base.parse :as parse]
[wolframite.runtime.defaults :as defaults]
[wolframite.base.convert :as convert]
[wolframite.base.evaluate :as evaluate]
[wolframite.base.express :as express]))

(comment

Expand All @@ -17,11 +20,11 @@

;; Low-level evaluate of a Wolfram expression (here, `Plus[1,2]`)
;; You normally don't want this - just use wl/eval. Notice that all the connection details explicitly specified.
((parse/parse-fn 'Plus (merge {:kernel/link @wl/kernel-link-atom}
((parse/parse-fn 'Plus (merge {:jlink-instance (jlink-instance/get)}
defaults/default-options))
1 2)
1 2))


)

;; * Clojure a 1min intro
;; ** Interactive developement
Expand Down Expand Up @@ -52,7 +55,7 @@
:project.media/image


;; *** Interesting compund datatypes
;; *** Interesting compound datatypes

;; vector

Expand All @@ -66,26 +69,26 @@
;; * Wolframite (nee Clojuratica) -- name is a WIP
;; ** Init (base example)

(wl/eval '(Dot [1 2 3] [4 5 6]))
(wl/eval (w/Dot [1 2 3] [4 5 6]))

;; ** Strings of WL code

(wl/eval "{1 , 2, 3} . {4, 5, 6}")
;; (convert/convert "{1 , 2, 3} . {4, 5, 6}" {:kernel/link @wl/kernel-link-atom}) ;; FIXME: this doesn't work, probably shouldn't, maybe there is a path to remove in `convert/convert`
;; (convert/convert "{1 , 2, 3} . {4, 5, 6}" {:jlink-instance (wolframite.impl.jlink-instance/get)}) ;; FIXME: this doesn't work, probably shouldn't, maybe there is a path to remove in `convert/convert`
;; You should use this
;; (express/express "{1 , 2, 3} . {4, 5, 6}" {:kernel/link @wl/kernel-link-atom})
;; (express/express "{1 , 2, 3} . {4, 5, 6}" {:jlink-instance (wolframite.impl.jlink-instance/get)})

;; NOTE: this is a hack to get this to work

;; ** Def / intern WL fns, i.e. effectively define WL fns as clojure fns:

(def W:Plus (parse/parse-fn 'Plus {:kernel/link @wl/kernel-link-atom}))
(def W:Plus (parse/parse-fn 'Plus {:jlink-instance (jlink-instance/get)}))

(W:Plus 1 2 3) ; ... and call it
(W:Plus 1 2 3) ; ... and call it (delegating its evaluation to the Wolfram kernel)

(def greetings
(wl/eval
'(Function [x] (StringJoin "Hello, " x "! This is a Mathematica function's output."))))
(w/fn [x] (w/StringJoin "Hello, " x "! This is a Mathematica function's output."))))

(greetings "Stephen")

Expand All @@ -101,13 +104,13 @@

;; *** Init math canvas & app

(def canvas (graphics/make-math-canvas! @wl/kernel-link-atom))
(def canvas (graphics/make-math-canvas!))
(def app (graphics/make-app! canvas))

;; *** Draw Something!

(graphics/show! canvas (wl/->wl '(GridGraph [5 5]) {:output-fn str}))
(graphics/show! canvas (wl/->wl '(ChemicalData "Ethanol" "StructureDiagram") {:output-fn str}))
(graphics/show! canvas (wl/->wl (w/GridGraph [5 5]) {:output-fn str}))
(graphics/show! canvas (wl/->wl (w/ChemicalData "Ethanol" "StructureDiagram") {:output-fn str}))

;; *** Make it easier (Dev Helper: closing over the canvas)

Expand All @@ -116,21 +119,21 @@

;; *** Some Simple Graphiscs Examples

(quick-show '(ChemicalData "Ethanol" "StructureDiagram"))
(quick-show '(GridGraph [5 5]))
(quick-show '(GeoImage (Entity "City" ["NewYork" "NewYork" "UnitedStates"])))
(quick-show (w/ChemicalData "Ethanol" "StructureDiagram"))
(quick-show (w/GridGraph [5 5]))
(quick-show (w/GeoImage (w/Entity "City" ["NewYork" "NewYork" "UnitedStates"])))


;; ** More Working Examples

(wl/eval '(GeoNearest (Entity "Ocean") Here))
(wl/eval (w/GeoNearest (w/Entity "Ocean") 'Here))

(wl/eval '(TextStructure "The cat sat on the mat."))
(wl/eval (w/TextStructure "The cat sat on the mat."))

;; - Wolfram Alpha

(wl/eval '(WolframAlpha "number of moons of Saturn" "Result"))
(wl/eval (w/WolframAlpha "number of moons of Saturn" "Result"))

;; - more interesting examples...

(wl/eval '(TextStructure "You can do so much with the Wolfram Language." "ConstituentGraphs")) ; returns a graph as data
(wl/eval (w/TextStructure "You can do so much with the Wolfram Language." "ConstituentGraphs")) ; returns a graph as data
47 changes: 21 additions & 26 deletions dev/explainer.clj
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
(ns explainer
"A demo ns used for explaining the basics of using Wolframite.
See also the demo ns."
See also the demo ns." ;; FIXME how does demo and this one differ?
(:require
[wolframite.core :as wl]
[wolframite.wolfram :as w]
[wolframite.tools.graphics :as graphics]
[wolframite.base.parse :as parse :refer [custom-parse]]
[wolframite.lib.helpers :as h]))
Expand Down Expand Up @@ -32,7 +33,7 @@

(eval '(map (fn [x] (+ x 1)) [1 2 3])) ; clojure eval

(wl/eval '(Map (fn [x] (+ x 1)) [1 2 3])) ; Wolframite eval
(wl/eval (w/Map (fn [x] (w/+ x 1)) [1 2 3])) ; Wolframite eval ;; FIXME(jakub) broken, doesn't call +; likely b/c we send fail to process the fn body correctly

(wl/eval "Map[Function[{x}, x + 1], {1, 2, 3}]") ; can also pass in Wolfram as string

Expand All @@ -46,23 +47,17 @@

(def greetings
(wl/eval
'(Function [x] (StringJoin "Hello, " x "! This is a Mathematica function's output."))))
(w/fn [x] (w/StringJoin "Hello, " x "! This is a Mathematica function's output."))))

(greetings, "folks") ; => "Hello, folks! This is a Mathematica function's output."

;; ** create a var for each Wolfram symbol, with docstrings, which resolves into a symbol suitable for `wl/eval`:

;; BEWARE: This is somewhat slow (few seconds on my PC)
(wl/load-all-symbols 'w) ; intern all Wolfram functions and entities into the namespace `w` as vars, with docstrings
(wl/load-all-symbols (.name *ns*)) ; some, but into the current ns

(def age 42)
;; Without symbols loaded, when we need interpolation, we need to deal with ` and prevent namespacing of symbols:
(wl/eval `(~'Floor (~'Plus ~age ~'Pi))) ; => 45
;; With loaded symbols, we can use them as-is
;; (Calva will even show their docstrings; clj-kondo and IntelliK though complain about unknown vars, as of now :'( )
;; But better to use loaded symbols:
(wl/eval (w/Floor (w/Plus age w/Pi))) ; => 45
(wl/eval (Floor (Plus age Pi))) ; => 43

;; *** REPL - load-all-symbols includes docstrings, so we can use repl to show them

Expand All @@ -75,7 +70,7 @@
(repl/apropos #"(?i)geo")

(h/help! 'Axes) ;; open a Wolfram docs page for Axes
(h/help! Axes) ; thx to loaded symbols, this works too
(h/help! w/Axes) ; thx to loaded symbols, this works too

;; Liks to all the symbols in this form:
(h/help! '(Take
Expand All @@ -86,51 +81,51 @@
(GenomeData)))
n)
:return-links true)
(h/help! (Take
(Sort
(Map
(Function ['gene]
[(GenomeData 'gene "SequenceLength") 'gene])
(GenomeData)))
(h/help! (w/Take
(w/Sort
(w/Map
;(w/Function ['gene] [(w/GenomeData 'gene "SequenceLength") 'gene])
(w/fn [gene] [(w/GenomeData gene "SequenceLength") gene])
(w/GenomeData)))
'n)
:return-links true)

(wl/eval (Information GenomeData))
(wl/eval (w/Information w/GenomeData))

(wl/eval '((WolframLanguageData "GenomeData") "Association")) ; FIXME broken? returns a few 'Missing "NotAvailable'

;; * Graphics

;; Init
(def canvas (graphics/make-math-canvas! @wl/kernel-link-atom))
(def canvas (graphics/make-math-canvas!))
(def app (graphics/make-app! canvas))
(defn quick-show [clj-form]
(graphics/show! canvas (wl/->wl clj-form {:output-fn str})))

(quick-show '(ChemicalData "Ethanol" "StructureDiagram"))
(quick-show '(GridGraph [5 5]))
(quick-show '(GeoImage (Entity "City" ["NewYork" "NewYork" "UnitedStates"])))
(quick-show (w/ChemicalData "Ethanol" "StructureDiagram"))
(quick-show (w/GridGraph [5 5]))
(quick-show (w/GeoImage (w/Entity "City" ["NewYork" "NewYork" "UnitedStates"])))

;; * Custom Parse

(wl/eval '(Hyperlink "foo" "https://www.google.com"))
(wl/eval (w/Hyperlink "foo" "https://www.google.com"))

(defmethod custom-parse 'Hyperlink [expr opts]
(-> (second (.args expr))
(parse/parse opts)
java.net.URI.))

(wl/eval '(Hyperlink "foo" "https://www.google.com"))
(wl/eval (w/Hyperlink "foo" "https://www.google.com"))
;; * More

;; WordFrequency[ExampleData[{"Text", "AliceInWonderland"}], {"a", "an", "the"}, "CaseVariants"]

(wl/eval (WordFrequency (ExampleData ["Text" "AliceInWonderland"]) ["a" "an" "the"] "CaseVariants"))
(wl/eval (w/WordFrequency (w/ExampleData ["Text" "AliceInWonderland"]) ["a" "an" "the"] "CaseVariants"))

;; Wrapping in a function
(defn wf [& {:keys [prop]
:or {prop "CaseVariants"}}]
(wl/eval (WordFrequency (ExampleData (conj ["Text"] "AliceInWonderland"))
(wl/eval (w/WordFrequency (w/ExampleData (conj ["Text"] "AliceInWonderland"))
(vector "a" "an" "the")
prop)))

Expand Down
Loading

0 comments on commit 39789bb

Please sign in to comment.