summaryrefslogtreecommitdiff
path: root/ob-spice.el
diff options
context:
space:
mode:
Diffstat (limited to 'ob-spice.el')
-rw-r--r--ob-spice.el283
1 files changed, 283 insertions, 0 deletions
diff --git a/ob-spice.el b/ob-spice.el
new file mode 100644
index 0000000..f3dc9c0
--- /dev/null
+++ b/ob-spice.el
@@ -0,0 +1,283 @@
+;;; ob-spice.el --- Babel Functions for spice
+;;; -*- coding: utf-8 -*-
+
+;; License: GPL v3, or any later version
+;;
+;; This file is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Org-Babel support for evaluating spice.
+
+;;; Requirements:
+
+;; - ngspice :: http://ngspice.sourceforge.net/
+
+;;; Code:
+
+(require 'ob)
+
+(make-comint "spice" "ngspice")
+
+(defvar org-babel-spice-eoe-indicator ":org_babel_spice_eoe"
+ "String to indicate that evaluation has completed.")
+
+(defvar org-babel-spice-command "ngspice"
+ "Name of command to use for executing ngspice.")
+(defun org-babel-spice-initiate-session (&optional session dir _params)
+ "Initiate a ngspice session.
+Create comint buffer SESSION running ngspice starting in
+default-directory or DIR if specified."
+ (let* ((sessionname (if (or (not session) (string= session "none"))
+ "spice" session))
+ (session (make-comint sessionname org-babel-spice-command)))
+ (if (and dir (file-name-absolute-p dir))
+ ;; absolute dir
+ (comint-simple-send session (format "cd %s" dir))
+ ;; relative dir
+ (comint-simple-send session (format "cd %s" default-directory))
+ session
+ )))
+(defun org-babel-prep-session:spice (session params)
+ "Prepare SESSION according to header arguments in PARAMS."
+ (let ((session (org-babel-spice-initiate-session session))
+ (var-lines (org-babel-variable-assignments:spice params)))
+ (org-babel-comint-in-buffer session
+ (sit-for .5) (goto-char (point-max))
+ (mapc (lambda (var)
+ (insert var) (comint-send-input nil t)
+ (org-babel-comint-wait-for-output session)
+ (sit-for .1) (goto-char (point-max))) var-lines))
+ session))
+(defun org-babel-load-session:spice (session body params)
+ "Load BODY into SESSION."
+ (save-window-excursion
+ (let ((buffer (org-babel-prep-session:spice session params)))
+ (with-current-buffer buffer
+ (goto-char (process-mark (get-buffer-process (current-buffer))))
+ (insert (org-babel-chomp body)))
+ buffer)))
+
+;; helper
+
+(defun org-babel-variable-assignments:spice (params)
+ "Return a list of spice statements to set the variables in PARAMS."
+ (mapcar
+ (lambda (pair)
+ (format "set %s=%s"
+ (car pair)
+ (org-babel-spice-var-to-spice (cdr pair))))
+ (org-babel--get-vars params)))
+(defun org-babel-spice-var-to-spice (var)
+ "Convert VAR into a spice variable."
+ (if (listp var)
+ (concat "( " (mapconcat #'org-babel-spice-var-to-spice var " ") " )")
+ (format "%S" var)))
+
+;; (lambda (text) (setq body (concat text "\n" body)))
+(defun org-babel-spice-vector-search (body vars)
+ "Replace first instance in BODY for all VARS."
+ (mapc (lambda (pair)
+ (if (string-match (format
+ "\\$%s\\[\\([0-9]\\)\\]"
+ (car pair))
+ body)
+ (let ((replacement (nth
+ (string-to-number (match-string 1 body))
+ pair)))
+ (setq body(format "%s%s%s"
+ (substring body 0 (match-beginning
+ 0))
+ replacement
+ (substring body (match-end 0)))))))
+ vars)
+ body
+ )
+
+(defun org-babel-spice-replace-vars (body vars)
+ "Expand BODY according to VARS."
+ (let ((old-body ""))
+ ;; replace vector variables preceded by '$' and followed by the
+ ;; index in square brackets starting at 0. Matches without
+ ;; preceding or succeeding spaces.
+ (while (not (string= old-body body))
+ (setq old-body body)
+ (setq body (org-babel-spice-vector-search body vars))
+ )
+ ;; replace any variable names preceded by '$' with the actual
+ ;; value of the variable. Matches only with succeeding space or
+ ;; end of line to prevent namespace limitations.
+ (mapc (lambda (pair)
+ (setq body (replace-regexp-in-string
+ (format "\\$%s\\( \\)\\|\\$%s$" (car pair)
+ (car pair))
+ (format "%s\1" (cdr pair))
+ body)))
+ vars)
+ body))
+(defun org-babel-expand-body:spice (body params)
+ "Expand BODY according to PARAMS, return the expanded body."
+ (let ((vars (org-babel--get-vars params))
+ (prologue (cdr (assq :prologue params)))
+ (epilogue (cdr (assq :epilogue params)))
+ (file (cdr (assq :file params))))
+ (setq body (org-babel-spice-replace-vars body vars))
+ ;; TODO :file stuff ....
+
+ ;; add prologue/epilogue
+ (when prologue (setq body (concat prologue "\n" body)))
+ (when epilogue (setq body (concat body "\n" epilogue)))
+ body))
+
+(defun org-babel-spice-trim-body (body)
+ "Prepare BODY to be used in interactive ngspice session."
+ ;; random control codes after $var inserts
+ (replace-regexp-in-string
+ "" " "
+ ;; .control .endc lines
+ (replace-regexp-in-string
+ "^ *\\.\\(control\\|endc\\) *$" ""
+ ;; comment lines
+ (replace-regexp-in-string
+ "^ *\\*.*$" "" body))))
+(defun org-babel-execute:spice (body params)
+ "Execute a block of Spice code with Babel.
+This function is called by `org-babel-execute-src-block'."
+ (let* (;(body (org-babel-expand-body:spice body params))
+ (gnuplot (cdr (assq :gnuplot params)))
+ (result-params (cdr (assq :result-params params)))
+ (result-type (cdr (assq :result-type params)))
+ (session (org-babel-spice-initiate-session
+ (cdr (assq :session params))
+ (cdr (assq :dir params))))
+ (vars (org-babel--get-vars params))
+ (no-source (cdr (assq :no-source params)))
+ (break-index (if (string-match "^ *\.end *$" body)
+ (match-end 0) 0))
+ ;;vars need to be replaced as they don't work when using source
+ (circuit-body (org-babel-expand-body:spice
+ (substring body 0 break-index)
+ (assq-delete-all :epilogue (copy-alist params))))
+ ;; todo: replace vars. :-( → set vars break when doing something like $file.txt
+ (control-body (org-babel-spice-trim-body (substring body break-index)))
+ (full-control-body (if (not (string= control-body ""))
+ (org-babel-expand-body:generic
+ control-body
+ (assq-delete-all :prologue (copy-alist params))
+ (org-babel-variable-assignments:spice params)) ""))
+ (circuit-file (if circuit-body (org-babel-temp-file "spice-body-" ".cir")))
+ (result))
+
+
+ (message (concat "circuit:\n" circuit-body))
+ (message (concat "\n-----\ncontrol:\n" control-body))
+
+ ;; Source circuit-body
+ (with-temp-file circuit-file (insert circuit-body))
+ ;; Evaluate
+ (setq result (org-babel-spice-evaluate session full-control-body
+ result-type circuit-file result-params))
+
+ ;; TODO deal with temporary files
+
+ ;;(org-babel-eval "ngspice -b " body)
+ ;; Write body to temp file & execute with ngspice comint buffer and ~source file~
+
+
+ ;; TODO read outputs from files
+
+ ;; TODO gnuplot options
+ (if (string= "yes" gnuplot)
+ nil ;return content(!) of gnuplot.plt for :post processing with gnuplot block?
+ nil ;return normal spice output
+ )
+ result
+ ))
+(defun org-babel-spice-source (buffer file)
+ "Source FILE in ngspice process running in BUFFER and return results."
+ (let ((body (concat "source " file)))
+ (org-babel-spice-evaluate buffer body 'value)))
+(defun org-babel-spice-evaluate (buffer body result-type &optional file result-params)
+ "Use BUFFER running ngspice process to eval BODY and return results.
+If RESULT-TYPE equals `output' return all outputs, if it equals
+`value' return only value of last statement. FILE can refer to a
+spice input file that is sourced before BODY execution is started."
+ (let ((eoe-string (format "echo \"%s\"" org-babel-spice-eoe-indicator))
+ (eval-body (if file (concat "source " file "\n" body) "")))
+ (pcase result-type
+ (`output
+ ;; Force session to be ready
+ ;;(org-babel-comint-with-output
+ ;; (buffer org-babel-spice-eoe-indicator t eoe-string)
+ ;; (insert eoe-string) (comint-send-input nil t))
+ ;; Eval body
+ (org-babel-chomp
+ (replace-regexp-in-string
+ "^Current directory: .*\n" ""
+ (replace-regexp-in-string
+ "^\\(ngspice [0-9]+ -> *\n*\\)*" ""
+ (mapconcat
+ #'identity
+ (butlast
+ (cdr
+ (split-string
+ (mapconcat
+ #'org-trim
+ (org-babel-comint-with-output (buffer org-babel-spice-eoe-indicator t eval-body)
+ (mapcar (lambda (line)
+ (insert (org-babel-chomp line)) (comint-send-input nil t))
+ (list eval-body
+ eoe-string
+ "\n")))
+ "\n") "[\r\n]")) 2) "\n"))))
+ )
+ (`value
+ (let ((tmp-file (org-babel-temp-file "spice-")))
+ (org-babel-comint-with-output
+ (buffer org-babel-spice-eoe-indicator t eval-body)
+ (mapcar
+ (lambda (line)
+ (insert (org-babel-chomp line)) (comint-send-input nil t))
+ (append (list eval-body)
+ (list (format "echo !! > %s" tmp-file)
+ (format "echo \"%s\"" org-babel-spice-eoe-indicator)
+ )))
+ (comint-send-input nil t))
+ ;; split result to output multiple comma separated vars as table
+ (let ((result (org-babel-spice-cleanup-result
+ (org-babel-chomp
+ (org-babel-eval-read-file tmp-file)))))
+ (if (or (not (listp result)) (cdr result))
+ result
+ (car result))
+ )))
+ ;;todo: add "smart" result type to display measurements (or echos?) & plot filenames
+ )))
+(defun org-babel-spice-cleanup-result (result)
+ "Cleanup value to return instead of RESULT.
+Commands that write to files return the filename."
+ (let* ((index (if (string-match "^ *[^ ]*" result)
+ (match-end 0) 0))
+ (type (substring result 0 index))
+ (arg (replace-regexp-in-string "^ *[^ ]* \\([^ ]*\\).*" "\\1" result)))
+ (message type)
+ (message arg)
+ (pcase type
+ ((or "wrdata" "write") arg)
+ ("gnuplot" (format "%s.png" arg))
+ ("echo" (split-string (substring result (+ index 1)) ","))
+ (_ result))))
+
+ (provide 'ob-spice)
+;;; ob-spice.el ends here