view sim.py @ 20:3341ef03cb66

Add PWM modulator
author Daniel O'Connor <darius@dons.net.au>
date Mon, 27 Nov 2023 13:19:37 +1030
parents 28475b505f1f
children
line wrap: on
line source

#!/usr/bin/env python3

# Copyright (c) 2023 Daniel O'Connor <darius@dons.net.au
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.

# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

import datetime
import pathlib
from scipy.optimize import differential_evolution
from spicelib.simulators.ltspice_simulator import LTspice
from spicelib.log.ltsteps import LTSpiceLogReader
from spicelib.editor.spice_editor import SpiceEditor
#from spicelib.sim.sim_runner import SimRunner
import shlex
import sqlite3
import subprocess

def simulate(src, simexe, simflags, rundir, runname, params):
    src = pathlib.Path(src)
    runname = pathlib.Path(runname)
    assert(runname.suffix == '.net')
    rundir = pathlib.Path(rundir)

    e = SpiceEditor(src)
    for k in params:
        e.set_parameter(k, params[k])

    #s = SimRunner(simulator = LTspice, output_folder = 'tmp', verbose = True)
    #s.simulator.spice_exe = [simexe]
    #raw_file, log_file = s.run_now(e, run_filename = runname)

    e.write_netlist(rundir / runname)

    cmd = [simexe, '-b']
    cmd.extend(simflags)
    cmd.append(runname.name)
    then = datetime.datetime.now()
    #print(f'Starting run {runname} at {then}')
    p = subprocess.Popen(cmd, cwd = rundir)
    p.communicate()
    now = datetime.datetime.now()
    taken = now - then

    if p.returncode != 0:
        raise Exception(' '.join(map(shlex.quote, cmd)) + ' failed with code ' + str(p.returncode))

    #rawpath = rundir / runname.with_suffix('.raw')
    #raw = spicelib.RawRead(rawpath)
    logpath = rundir / runname.with_suffix('.log')
    log = LTSpiceLogReader(logpath)

    power = log.get_measure_value('pout')
    eff = log.get_measure_value('efficiency')
    thd = log.fourier['V(rfout)'][0].thd
    ipeak_u2 = log.get_measure_value('ipeak_u2')
    ipeak_u5 = log.get_measure_value('ipeak_u5')
    ipeak = max(ipeak_u2, ipeak_u5)

    return then, taken, power, eff, thd, ipeak

def calccost(power, eff, thd, ipeak):
    # Calculate the cost
    tpwr = 1500
    tthd = 2
    teff = 90
    imax = 11

    cost = 0
    if power < tpwr * 0.80:
        cost += 100
    elif power < tpwr:
        cost += (tpwr - power) / tpwr / 100

    if thd > 5 * tthd:
        cost += 100
    else:
        thdinv = 100 - thd
        tthdinv = 100 - tthd
        if thdinv < tthdinv:
            cost += (tthdinv - thdinv) / tthdinv
    if eff < teff:
        cost += (teff - eff) / teff

    if ipeak > imax:
        cost += (ipeak - imax) * 5

    return cost

def fn(v, dsn, simexe, simflags, rundir, circ):
    '''Called by differential_evolution, v contains the evolved parameters, the rest are passed in from the args parameter'.
    Returns the cost value which is being minimised'''

    # Get parameters for run
    duty, c1, c2, l1, l2 = v

    # Check if this combination has already been tried
    dbh = sqlite3.connect(dsn)
    cur = dbh.cursor()
    cur.execute('SELECT run, power, efficiency, thd, ipeak FROM GAN190 WHERE name = ? AND duty = ? AND c1 = ? AND c2 = ? AND l1 = ? AND l2 = ?',
                (circ, duty, c1, c2, l1, l2))
    tmp = cur.fetchone()
    if tmp is not None:
        run, power, eff, thd, ipeak = tmp
        # Recalculate the cost since it is cheap and might have been tweaked since the original run
        cost = calccost(power, eff, thd, ipeak)
        print(f'Found run {run:3d}: Duty {duty:3.0f}%, C1 {c1:3.0f}pF, C2 {c2:3.0f}pF, L1 {l1:3.0f}uH, L2 {l2:4.0f}nH -> Power: {power:6.1f}W Efficiency: {eff:5.1f}% THD: {thd:5.1f}% IPeak: {ipeak:4.1f}A Cost: {cost:6.2f}')
        return cost

    # Get next run number
    cur.execute('BEGIN DEFERRED')
    cur.execute('INSERT INTO GAN190 DEFAULT VALUES RETURNING rowid')
    run = cur.fetchone()[0]
    cur.execute('COMMIT')

    # Run the simulation
    # Need to convert units to suit
    runname = pathlib.Path(circ).stem + '-' + str(run) + '.net'
    try:
        then, taken, power, eff, thd, ipeak = simulate(circ, simexe, simflags, rundir, runname,
                                                       {'dutypct' : duty, 'C1' : c1 * 1e-12, 'C2' : c2 * 1e-12, 'L1' : l1 * 1e-6, 'L2' : l2 * 1e-9})
    except:
        return 100000
    # Calculate the cost
    cost = calccost(power, eff, thd, ipeak)

    # Log & save the results
    print(f'Run {run:3d}: Duty {duty:3.0f}%, C1 {c1:3.0f}pF, C2 {c2:3.0f}pF, L1 {l1:3.0f}uH, L2 {l2:4.0f}nH -> Power: {power:6.1f}W Efficiency: {eff:5.1f}% THD: {thd:5.1f}% IPeak: {ipeak:4.1f}A Cost: {cost:6.2f}')
    taken = taken.seconds + taken.microseconds / 1e6
    cur.execute('BEGIN DEFERRED')
    cur.execute('REPLACE INTO GAN190 (name, run, start, duration, duty, c1, c2, l1, l2, power, efficiency, thd, ipeak, cost) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
                 (circ, run, then, taken, duty, c1, c2, l1, l2, power, eff, thd, ipeak, cost))
    cur.execute('COMMIT')

    return cost

def ev(circ):
    # Bounds for parameters
    # Note that the parameters are also constrained to be integral
    bounds = [(10, 80),
              (1, 20),
              (10, 300),
              (1, 20),
              (10, 500)]

    # Initial solution
    x0 = [36, 10, 155, 5, 140]

    # Where to save results
    dsn = 'results.db'
    dbh = sqlite3.connect(dsn)
    cur = dbh.cursor()
    cur.execute('''
CREATE TABLE IF NOT EXISTS GAN190 (
    name	TEXT,		-- Circuit name
    run		INTEGER,	-- Run number
    start	DATETIME,	-- Datetime run started
    duration	REAL,		-- Length of run (seconds)
    duty	REAL,		-- Duty cyle (%)
    c1		REAL,		-- Value of C1 (pF)
    c2		REAL,		-- Value of C2 (pF)
    l1		REAL,		-- Value of L1 (uH)
    l2		REAL,		-- Value of L2 (nH)
    power	REAL,		-- Measured power (Watts)
    efficiency	REAL,		-- Measured efficiency (%)
    thd		REAL,		-- Total harmonic distortion (%)
    ipeak	REAL,		-- Peak drain current (A)
    cost	REAL		-- Calculated cost metric
);''')

    return differential_evolution(fn, bounds, x0 = x0,
                                  args = (dsn, '/Users/oconnd1/bin/runltspice', [], 'tmp', circ),
                                  integrality = True, disp = True, seed = 12345, updating = 'deferred', workers = 4)