#+PROPERTY: header-args:emacs-lisp :tangle ob-spice-exp.el :results silent * License #+BEGIN_SRC emacs-lisp ;;; 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) #+END_SRC * Functionality - Temporary files in working directory are accepted (as they are common in ngspice) - With no :file specified try to direct all outputs to a /tmp directory - option to return content of generated gnuplot .plt for easy use with :post {generic gnuplot block} - Full .plt or just data points? ** [0/4] Flags *** NEXT [#C] :netlist Name of src block to include for netlist *** NEXT [#B] :dir working directory to start ngspice in *** NEXT [#B] :file? default filename to use for *** NEXT :gnuplot name of gnuplot block or .plt file to prepend to each plot ** DONE Variable replacement in spice body ** INPROGRESS Execution using ngspice *** NEXT Steps to using interactive mode - Output to a rawfile dumps all node data: ~ngspice -b -r x.raw y.cir~. Can be loaded in interactive mode with ~load filename~. - Run file in interactive mode syncronously ~source input-file~ or asyncronously ~aspice input-file~ **** Comint mode to run ngspice in a buffer #+BEGIN_SRC emacs-lisp (make-comint "spice" "ngspice") #+END_SRC ** PLANNING (Auto-)Plotting * Code #+BEGIN_SRC emacs-lisp :tangle no (add-to-list 'load-path "~/.emacs.d/lisp/ob-spice") #+END_SRC ** vars #+BEGIN_SRC emacs-lisp (defvar org-babel-spice-eoe-indicator ":org_babel_spice_eoe" "String to indicate that evaluation has completed.") #+END_SRC ** Session handling #+BEGIN_SRC emacs-lisp (defvar org-babel-spice-command "ngspice" "Name of command to use for executing ngspice.") (defun org-babel-spice-initiate-session (&optional session _params) "Initiate a ngspice session. Create comint buffer SESSION running ngspice." (let ((sessionname (or session "spice"))) ;; set default-directory to start process in other than current dir (make-comint sessionname org-babel-spice-command))) (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))) #+END_SRC ** NEXT Variable handling & expand body Don't replace variable calls in body. Instead set them in the spice session: ~set x=4~\\ ~set~ only sets lowercase variants of words! To set lists/arrays: ~set x=( 1 2 3 4 )~. Whitespace is important! Access with ~$x[0]~ or ~$x[2-len]~. #+BEGIN_SRC emacs-lisp ;; (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)) (cadr pair)))) (setq body(format "%s%s%s" (substring body 0 (match-beginning 0)) replacement (substring body (match-end 0))))))) vars) body ) #+END_SRC #+BEGIN_SRC emacs-lisp (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-exp (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))) (old-body "")) (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)) #+END_SRC ** ob-execute #+BEGIN_SRC emacs-lisp (defun org-babel-execute:spice-exp (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-exp body params)) (gnuplot (cdr (assq :gnuplot params))) (session (org-babel-spice-initiate-session (cdr (assq :session 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)) (circuit-body (org-babel-spice-replace-vars (substring body 0 break-index) vars));vars need to be replaced as they don't work when using source ;; todo: remove comments & .control .endc lines ;; todo: replace vars. :-( → set vars break when doing something like $file.txt (control-body (substring body break-index)) (full-control-body (if control-body (org-babel-expand-body:generic control-body 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)) (message (buffer-name session)) ;; Source circuit-body (with-temp-file circuit-file (insert circuit-body)) (org-babel-spice-source session circuit-file) ;; Run control-body (setq result (org-babel-spice-evaluate session full-control-body)) ;; 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 or ;nowebbing spice into gnuplot 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))) (defun org-babel-spice-evaluate (buffer body) "Pass BODY to ngspice process in BUFFER and return results." (let ((eoe-string (format "echo \"%s\"" org-babel-spice-eoe-indicator))) ;; 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 (replace-regexp-in-string "^ngspice [0-9]+ -> " "" (mapconcat #'identity (butlast (cdr (split-string (mapconcat #'org-trim (org-babel-comint-with-output (buffer org-babel-spice-eoe-indicator t body) (mapcar (lambda (line) (insert (org-babel-chomp line)) (comint-send-input nil t)) (list body eoe-string "\n"))) "\n") "[\r\n]")) 2) "\n")) )) (provide 'ob-spice-exp) ;;; ob-spice.el ends here #+END_SRC * Tests #+BEGIN_SRC spice-exp :var x="4" :session spicetest :no-source yes echo "Hello World" #+END_SRC #+RESULTS: : Hello World #+BEGIN_SRC spice-exp :var file="/tmp/spice_test" :results drawer :session spicetest ,*Time Constant Measurement r1 1 0 10k c1 1 0 1p .IC V(1)=1 .tran 1n 0.1u .print tran v(1) .end .control run set gnuplot_terminal=png ,*gnuplot $file v(1) meas tran value_at_tau find V(1) at=1e-8 meas tran value_at_five_tau find V(1) at=5e-8 echo value_at_tau = "$&value_at_tau" > $file.txt ,* Any better way to write one value of vector to a file?? echo value_at_five_tau = "$&value_at_five_tau" >> $file.txt .endc #+END_SRC #+RESULTS: :RESULTS: ngspice 157 -> ngspice 157 -> .control: no such command available in ngspice Doing analysis at TEMP = 27.000000 and TNOM = 27.000000 Initial Transient Solution -------------------------- Node Voltage ---- ------- 1 1 No. of Data Rows : 108 ngspice 160 -> v(1)*gnuplot: no such command available in ngspice value_at_tau = 3.679797e-01 value_at_five_tau = 6.717322e-03 Error: file.txt: no such variable. Error: missing name for output. *: no such command available in ngspice Error: file.txt: no such variable. Error: missing name for output. .endc: no such command available in ngspice :END: