;;; 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 . ;;; 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