/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: IndirectSet.java
 * Written by Tom O'Neill.
 *
 * Copyright (c) 2004, Static Free Software. All rights reserved.
 *
 * Electric(tm) 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 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) 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/>.
 */
package com.sun.electric.tool.simulation.test;

/**
 * Adjusts voltage on a provided power supply channel until the specified
 * current reaches the desired set point. The current and voltage must be
 * correlated. Assumes the current is a monotonic function of voltage, and that
 * the derivative is always nonzero.
 * <p>
 * 
 * We use a class instead of just a method so that caller can obtain resulting
 * voltage and current without performing time-consuming readVoltage() and
 * readCurrent() invocataions.
 */
public class IndirectSet {

    /** Maximum number of convergence steps */
    public static final int MAX_VOLTS_STEPS = 100;

    /**
     * Maximum fraction by which we ever expect to overshoot the target used to
     * set range for <code>readCurrent()</code>.
     */
    public static final float MAX_CURRENT_OVERSHOOT = 5.f;

    /**
     * Fraction of <code>ampsError</code> to set <code>readCurrent()</code>
     * resolution to.
     */
    public static final float EXCESS_RESOLUTION = 0.01f;

    /** Best guess so far at voltage required to produce <code>setAmps</code> */
    public float volts;

    /** Current measured when voltage equals <code>volts</code> */
    public float amps;

    /**
     * True if successfully set <code>amps</code> to approximately
     * <code>setAmps</code>
     */
    public boolean achievedCurrent = false;

    // Desired current set point
    private float setAmps;

    // Current and voltage pairs bounding setAmps. Voltage should stay
    // in range 0..maxVolts
    private float ampsLow = -Float.MAX_VALUE, voltsLow = 0.f;

    private float ampsHigh = Float.MAX_VALUE, voltsHigh;

    // The current, voltage pairs closest to setAmps (warmer=closer).
    // Initially assume 0 Volts produces 0 A, won't hurt if it isn't true.
    private float ampsHot = 0.f, voltsHot = 0.f;

    private float ampsWarm = -Float.MAX_VALUE, voltsWarm = 0.f;

    // Derivative of voltage wrt current, and its value in previous iteration
    private float voltsPerAmp, oldVoltsPerAmp = 0.f;

    // Resolution of voltage setting
    private float voltsResolution;

    // Previous value of volts
    private float oldVolts;

    /**
     * Adjust the voltage on <code>knob</code> until current on
     * <code>dial</code> reaches the setpoint. A safe non-zero starting value
     * <code>initVolts</code> must be provided.
     * <p>
     * Assumes <code>knob</code> provides the correct resolution for the power
     * supply. If the current readback is noisier than <code>ampsError</code>,
     * the routine may fail.
     * 
     * @param dial
     *            object providing current readback
     * @param setAmps
     *            desired current setpoint
     * @param ampsError
     *            allowed deviation from setAmps
     * @param knob
     *            object providing voltage control
     * @param initVolts
     *            initial guess at correct voltage, must be non-zero
     * @param maxVolts
     *            maximum allowed voltage
     * @see PowerChannel#getVoltageResolution
     */
    public IndirectSet(CurrentReadable dial, float setAmps, float ampsError,
            PowerChannel knob, float initVolts, float maxVolts) {

        this.setAmps = setAmps;
        volts = initVolts;
        voltsHigh = maxVolts;

        // Safety limit on size of voltage change in a single iteration
        float maxVoltsStep = Math.abs(0.2f * initVolts);
        
        // Range and resolution for readCurrent()
        float ampsMax = MAX_CURRENT_OVERSHOOT * setAmps;
        float ampsResolution = EXCESS_RESOLUTION * ampsError;

        voltsResolution = knob.getVoltageResolution();

        for (int ind = 0; ind < 100; ind++) {
            volts = Math.round(volts / voltsResolution) * voltsResolution;
            knob.setVoltageWait(volts);
            amps = dial.readCurrent(ampsMax, ampsResolution);
            System.out.println(ind + ": " + volts + " V, " + amps + " A");
            float ampsDiff = Math.abs(amps - setAmps);

            //System.out.println(volts + " V, " + amps + " A");

            if (ampsDiff <= ampsError) {
                achievedCurrent = true;
                return;
            }

            // Tighten bounds around setAmps if possible. If bounds within
            // voltage resolution, choose better endpoint and return.
            if (updateBounds(knob)) {
                achievedCurrent = true;
                return;
            }

            // Update record of current, voltage pairs to setAmps
            updateClosePairs(setAmps, ampsDiff);

            // Find slope of current-voltage relationship
            if (!updateVoltsPerAmp(ind)) {
                return;
            }

            // Compute new voltage assuming linear current-voltage relationship
            computeNextVolts(maxVoltsStep);

            // If new voltage outside bound around setAmps, then try the
            // midpoint of the bounded region instead
            if (volts < voltsLow || volts > voltsHigh) {
                System.err.println("Warning: volts=" + volts
                        + " outside bounds " + voltsLow + ".." + voltsHigh);
                volts = 0.5f * (voltsLow + voltsHigh);
            }

            System.out.println(ind + ": " + getState() + "\n");
            if (volts <= 0.f || volts > maxVolts) {
                Infrastructure.nonfatal("Voltage not converging, reached "
                        + volts + "V");
                return;
            }
        }

        Infrastructure.nonfatal("Voltage did not converge in "
                + IndirectSet.MAX_VOLTS_STEPS + " steps");
        return;
    }

    /**
     * Returns a string giving the complete state of the
     * {@link IndirectSet} object.
     *  
     * @return string giving the complete state of the object
     */
    public String getState() {
        String state = volts + " V, " + amps + " A; old=" + oldVolts + " V";
        state += "\n  low: " + voltsLow + " V, " + ampsLow + " A;";
        state += " high: " + voltsHigh + "V, " + ampsHigh + " A;";
        state += "\n  hot: " + voltsHot + "V, " + ampsHot + " A;";
        state += " warm: " + voltsWarm + "V, " + ampsWarm + " A;";
        state += "\n  voltsPerAmp: " + voltsPerAmp;
        state += "V, oldVoltsPerAmp: " + oldVoltsPerAmp;

        return state;
    }

    // Compute new voltage assuming linear current-voltage relationship.
    // Ensures that the magnitued of the change is at most maxVoltsStep, and at
    // least one voltage resolution unit
    private void computeNextVolts(float maxVoltsStep) {
        float voltsStep = (setAmps - amps) * voltsPerAmp;

        if (Math.abs(voltsStep) > maxVoltsStep) {
            if (voltsStep >= 0.f) {
                voltsStep = maxVoltsStep;
            } else {
                voltsStep = -maxVoltsStep;
            }
        }

        oldVolts = volts;
        volts += voltsStep;
        volts = Math.round(volts / voltsResolution) * voltsResolution;
        if (volts == oldVolts) {
            if (voltsStep > 0.f) {
                volts += voltsResolution;
            } else {
                volts -= voltsResolution;
            }
        }
    }

    // Find slope of current-voltage relationship. Returns true if
    // voltsPerAmp has same sign as previous reading.
    private boolean updateVoltsPerAmp(int ind) {
        boolean status = true;
        voltsPerAmp = (voltsHot - voltsWarm) / (ampsHot - ampsWarm);
        if (ind > 1 && (voltsPerAmp > 0.f && oldVoltsPerAmp < 0.f)
                || (voltsPerAmp < 0.f && oldVoltsPerAmp > 0.f)) {
            Infrastructure.nonfatal("voltsPerAmp (dV/dI) =" + voltsPerAmp
                    + ", was " + oldVoltsPerAmp);
            status = false;
        }
        oldVoltsPerAmp = voltsPerAmp;
        return status;
    }

    // Update record of current, voltage pairs to setAmps
    private void updateClosePairs(float setAmps, float ampsDiff) {
        /* Note actual data always overrides assumed value at (0 V, 0 A) */
        if (ampsDiff < Math.abs(ampsHot - setAmps)
                || (ampsHot == 0.f && voltsHot == 0.f)) {
            ampsWarm = ampsHot;
            voltsWarm = voltsHot;
            ampsHot = amps;
            voltsHot = volts;
        } else if (ampsDiff < Math.abs(ampsWarm - setAmps)) {
            ampsWarm = amps;
            voltsWarm = volts;
        }
    }

    // Tighten bounds around setAmps if possible. If we have
    // gotten voltage as close as it can be, choose closer bounds endpoint
    // and return true. Otherwise return false.
    private boolean updateBounds(PowerChannel knob) {
        if (amps <= setAmps && amps > ampsLow) {
            ampsLow = amps;
            voltsLow = volts;
        }
        if (amps > setAmps && amps < ampsHigh) {
            ampsHigh = amps;
            voltsHigh = volts;
        }
        if (Math.abs(voltsHigh - voltsLow) <= (1.001f * voltsResolution)) {
            if (Math.abs(ampsLow - setAmps) < Math.abs(ampsHigh - setAmps)) {
                volts = voltsLow;
                amps = ampsLow;
            } else {
                volts = voltsHigh;
                amps = ampsHigh;
            }
            knob.setVoltageWait(volts);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
    }

}
