#+PROPERTY: header-args:emacs-lisp :tangle ob-spice.el :results silent #+PROPERTY: header-args:org :tangle readme.org :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 * Auto-tangle #+BEGIN_SRC emacs-lisp :tangle no (defun my/tangle-ob-spice () "If the current file is '~/.emacs.d/lisp/ob-spice/ob-spice.org', the code blocks are tangled" (when (equal buffer-file-name (concat (getenv "HOME") "/.emacs.d/lisp/ob-spice/ob-spice.org")) (org-babel-tangle) (message "%s tangled" buffer-file-name))) (add-hook 'after-save-hook #'my/tangle-ob-spice) #+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? ** Use cases *** DONE just load some circuit #+BEGIN_SRC spice :results output ,* RC r1 1 0 10k c1 1 0 1p .IC V(1)=1 .tran 1n 0.1u .end #+END_SRC #+RESULTS: : Circuit: * rc *** DONE execute some stuff #+BEGIN_SRC spice echo "Hello world" #+END_SRC #+RESULTS: : Hello world #+BEGIN_SRC spice echo "0,1,2,3" #+END_SRC #+RESULTS: | 0 | 1 | 2 | 3 | *** NEXT plot some voltages and return png #+BEGIN_SRC spice :var file="/tmp/xzy" :results file ,*RC circuit r1 1 0 10k c1 1 0 1p .IC V(1)=1 .tran 1n 0.1u .end .control run set gnuplot_terminal=png gnuplot $file v(1) ,* can't even use $file.png here echo /tmp/xzy.png .endc #+END_SRC #+RESULTS: [[file:/tmp/xzy.png]] *** DONE do measurements and return results #+BEGIN_SRC spice :session spicetest :results value ,*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 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_five_tau .endc #+END_SRC #+RESULTS: | 0.36798 | 0.00671732 | *** NEXT write simulation data to file and return file name(s?) #+BEGIN_SRC spice :var file="/tmp/xyz" :post plot_stuff(data=*this*) :results file ,*RC circuit r1 1 0 10k c1 1 0 1p .IC V(1)=1 .tran 1n 0.1u .end .control run wrdata $file v(1) ,* make this line not neccessary: echo $file .endc #+END_SRC #+RESULTS: [[file:/tmp/xyz_plot.png]] #+NAME: plot_stuff #+BEGIN_SRC gnuplot :var data="x" :file "/tmp/xyz_plot.png" :results silent plot data u 1:2 w l ls 1 #+END_SRC ** [1/6] Flags *** NEXT [#C] :netlist / :circuit Name of src block to include for netlist *** DONE [#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: handle plotting instead of gnuplot lines *** NEXT :output **** `smart display measurements (or echos?) & plot filenames *** NEXT :batch / :no-interactive use batch mode ** 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 * Readme.org #+BEGIN_SRC org ,* Overview Extends org-babel capabilities to support spice simulations using ngspice. Simulations are executed using an interactive ngspice process running in emacs. The running ngspice process can be used to manipulate the simulation results directly or spread a simulation into multiple src blocks. Spice source blocks are interpreted as circuit descriptions until an '.end' line is encountered. After that a control part may follow (surrounded by '.control' and '.endc' lines). *If there is no '.end'* ,*line all of the src block is interpreted as a control block!* ,* Use cases ,*** Just load some circuit for later simulation: ,#+BEGIN_SRC spice :results output ,,* RC r1 1 0 10k c1 1 0 1p .IC V(1)=1 .tran 1n 0.1u .end ,#+END_SRC ,#+RESULTS: : Circuit: * rc ,*** Execute some stuff ,#+BEGIN_SRC spice echo "Hello world" ,#+END_SRC ,#+RESULTS: : Hello world ,#+BEGIN_SRC spice echo "0,1,2,3" ,#+END_SRC ,#+RESULTS: | 0 | 1 | 2 | 3 | ,*** Plot some voltages and return png ,#+BEGIN_SRC spice :var file="/tmp/xzy" :results file ,,*RC circuit r1 1 0 10k c1 1 0 1p .IC V(1)=1 .tran 1n 0.1u .end .control run set gnuplot_terminal=png gnuplot $file v(1) echo /tmp/xzy.png .endc ,#+END_SRC ,#+RESULTS: [[file:/tmp/xzy.png]] ,*** Do measurements and return results ,#+BEGIN_SRC spice :session spicetest :results value ,,*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 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_five_tau .endc ,#+END_SRC ,#+RESULTS: | 0.36798 | 0.00671732 | ,*** write simulation data to file and return file name ,#+BEGIN_SRC spice :var file="/tmp/xyz" :post plot_stuff(data=*this*) :results file ,,*RC circuit r1 1 0 10k c1 1 0 1p .IC V(1)=1 .tran 1n 0.1u .end .control run wrdata $file v(1) echo $file .endc ,#+END_SRC ,#+RESULTS: [[file:/tmp/xyz_plot.png]] ,#+NAME: plot_stuff ,#+BEGIN_SRC gnuplot :var data="x" :file "/tmp/xyz_plot.png" :results silent plot data u 1:2 w l ls 1 ,#+END_SRC ,* Flags ,** :netlist / :circuit Name of a src block to include for netlist/circuit descriptions. ,** :dir Working directory to run the src block in. Default is the value of `default-directory'. ,** :file? ,** :gnuplot Name of gnuplot block or .plt file: handle plotting instead of gnuplot lines ,** :results Available result options are `value' which returns the output of the last expression, `output' which returns all output and `smart' which tries to only display echos and plot filenames. ,** :batch / :no-interactive use batch mode #+END_SRC * 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 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))) #+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 (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)) #+END_SRC ** ob-execute #+BEGIN_SRC emacs-lisp (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 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 (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 "!! > %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 (split-string (org-babel-chomp (org-babel-eval-read-file tmp-file)) ","))) (if (cdr result) result (car result)) ))) ;;todo: add "smart" result type to display measurements (or echos?) & plot filenames ))) (provide 'ob-spice) ;;; ob-spice.el ends here #+END_SRC * Tests #+BEGIN_SRC spice :var x="4" :session spicetest :no-source yes echo "Hello World" #+END_SRC #+RESULTS: : Hello World #+BEGIN_SRC spice :var file="/tmp/spice_test" :session spicetest :results value ,*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_tau ,$&value_at_five_tau .endc #+END_SRC #+RESULTS: | 0.36798 | 0.00671732 | #+BEGIN_SRC spice :var file="/tmp/spice_test2" :session spicetest :results output ,*Virtual Ground Test: opamp gain = 1000 vin in 0 dc 0V sin(0 .1 100Hz) r1 in inn 10k r2 inn out 10k EOpamp out 0 0 inn 1000 .tran 0.1ms 0.05s .print tran v(in) .meas tran vtest find v(in) at=0.04e-3 .end .control run set gnuplot_terminal=png gnuplot $file v(in) v(out) v(inn) .endc #+END_SRC #+RESULTS: #+begin_example Circuit: *virtual ground test: opamp gain = 1000 Doing analysis at TEMP = 27.000000 and TNOM = 27.000000 Initial Transient Solution -------------------------- Node Voltage ---- ------- in 0 inn 0 out 0 eopamp#branch 0 vin#branch 0 No. of Data Rows : 508 Measurements for Transient Analysis vtest = 2.512902e-03 #+end_example