# Define and validate polynomial current and charge

Use the Y designator and a POLY model, keep current and charge coefficient units explicit, and compare static, complex and transient outputs with independent equations.

## Compatibility

Fresh current-source Windows 2.9.0 review candidate; final-package/platform checks pending

Published guidance with release-applicability limits. Confirm package applicability before following this guide.

**KB-RESULT-001 · verified workaround**: Save the intended edited source before Run and verify current arrays. Choosing No runs the saved source; the document current label may then disagree with the still-edited buffer.

[Download Complete diode and polynomial current/charge teaching models (ZIP)](/_functions/kbExample?name=josim-nonlinear-models.zip)

Includes: diode-ac.cir, diode-dc.cir, diode-op.cir, diode-tran.cir, diode.inc, poly-ac.cir, poly-dc.cir, poly-op.cir, poly-tran.cir, poly.inc, run_models.py, README.txt.

## What this model represents

For positive voltage V(p)-V(n), positive device current flows from p to n. The terminal current is I(v) + dQ(v)/dt; its small-signal admittance about a selected bias is dI/dv + j*omega*dQ/dv. Keep static current, total transient current and an AC phasor distinct.

Use Yname p n model with .MODEL name POLY(...). B remains the Josephson-junction designator. I(v)=I0+I1*v+I2*v^2+I3*v^3 and Q(v)=Q0+Q1*v+Q2*v^2+Q3*v^3; coefficient units are A/V^r and C/V^r respectively. A constant Q0 has zero derivative and is not a capacitance.

## Prepare the complete teaching inputs

Use a compatible licensed JoSIM®-Pro 2.9.0 Windows runtime and a writable copy of the complete download. Keep the chosen model include with all four matching .cir inputs. The standalone Python 3 checker needs no third-party packages; set $Python, $JoSIM and $Licence to full paths. $Licence must be the issued JoSIM®-Pro file, distinct from an InductEx® licence.

These are synthetic model checks. Agreement does not calibrate a measured semiconductor, establish cryogenic validity or prove stability or uniqueness for another topology.

## Run and inspect the model in the GUI

1. Open poly-op.cir from the writable copy, confirm its saved source and include, then use Run simulation. Read the scalar values and units; an operating point is not a waveform.
2. Open and run poly-dc.cir. Its seven independently solved points sweep V1 from -0.1 V to 0.5 V in 0.1 V steps. The horizontal coordinate is source voltage, not elapsed time.
3. Open and run poly-ac.cir. The selected bias is 0.2 V; frequencies are 1, 2 and 3 GHz. The one-volt, 30-degree peak reference phasor multiplies the local admittance; it does not simulate a physically qualified one-volt nonlinear excursion.
4. Open and run poly-tran.cir with the explicit fixed-step VOLTAGE/BDF options. The voltage ramps from -0.1 V to 0.5 V over 2 ns, using 5 ps steps and output cadence. Read voltage and total current with their separate display units.
5. Before rerunning an edit, choose Yes to save and run the intended source. Cancel creates no new task. No uses the saved file; it does not simulate the unsaved text. The checked path can nevertheless label that document Results current, so verify saved input and current output identity.

![Actual seven-point polynomial DC plots include voltage, device/source currents and dimensionless operating-point diagnostics against the swept source voltage.](https://static.wixstatic.com/media/31de30_4b2c905ae2894ac0a325d900f2bf3b03~mv2.png)

The DC horizontal coordinate is swept source voltage, not time. OP.iterations and OP.normalized_residual are dimensionless diagnostics, not device signals.

## Interpret current and charge separately

At 0.2 V this polynomial gives 300 microamps, differential conductance 1.08 mS and differential capacitance 2.52 pF. At DC, dQ/dt is zero. During the ramp, add (Q1+2*Q2*v+3*Q3*v^2) times 3e8 V/s. The Q0=7p constant does not change transient current; changing it to zero reproduced the same arrays.

The initial sample contains the static initial current. Startup charge history and the final ramp corner are excluded from the differentiable-interior reference check; a plotted initial current jump is not itself proof of an error.

![Actual polynomial transient plots show a voltage ramp and total current including the derivative of charge; time is in nanoseconds and current in milliamperes.](https://static.wixstatic.com/media/31de30_2c94e3a724f54f6d82b5e3dae3ba34f0~mv2.png)

The first sample is the initial static current. The later current includes charge response to the ramp; the independently checked interior excludes startup and the final ramp corner.

## Compare time refinement with the independent reference

The checker also runs 20 ps and 2.5 ps copies without changing the canonical inputs. Coarse solver completion is distinct from the current-error criterion. For this fixture, 20 ps misses the criterion, while 5 ps and 2.5 ps meet it and the latter reduces the maximum interior error.

The polynomial interior bound is 10 nA. Maximum errors are approximately 64.8, 4.05 and 1.0125 nA at 20, 5 and 2.5 ps. The cubic-charge BDF2 truncation predicts 2*Q3*(3e8)^3*h^2, independently explaining this refinement pattern. The bound belongs to this supplied fixture, not arbitrary POLY models.

## Recognise limits and recover a rejected run

D/Y transients require fixed-step VOLTAGE/BDF in the checked runtime. PHASE and TRAP reject these transients; adaptive stepping also fails the shared runtime contract. Mixed Josephson transients and checkpoint pause/resume remain outside the documented model support.

Unknown model fields and unsupported instance attributes are errors. The checked controls reject AREA=0, FC=1 and a mismatched diode .TEMP; Y accepts the model name, not a diode AREA attribute. POLY negative finite coefficients are permitted, without a passivity guarantee.

A deliberately insufficient Newton iteration limit fails without a new static report and preserves a pre-existing authoritative report. Restoring the ordinary limit and rerunning at the same bias gives a fresh checked result. Correct the first diagnostic and use a new output generation; an iteration limit alone does not tighten an already accepted residual gate.

## Save the refinement edit deliberately

The native scenario checked Cancel, No and both explicit Yes/save choices. No retained the original 401-sample saved run while preserving the edited 20 ps buffer. Yes saved and ran that buffer, producing 101 samples. Normal Undo followed by another Yes/save and Run restored the original source and 401-sample arrays. A separately launched app explicitly reopening the saved input reproduced them.

KB-RESULT-001 retains the No-path document-label contradiction. Compare saved source, task receipt and output data; do not use Results current alone as proof that the visible unsaved text was executed.

![The actual unsaved-source dialog offers Yes to save and run, No to run the saved file, and Cancel to stop.](https://static.wixstatic.com/media/31de30_d2a0f424428b4c76999682fd38ca2235~mv2.png)

Choose Yes to run the visible edit. No can retain a stale task while the document incorrectly says Results current; verify source identity and the new numerical data.

## Check the polynomial teaching model

Run in the extracted input directory. The output root must be new. The helper records the actual version, source and output hashes, isolates its own configuration and leaves the issued licence unchanged. Raw logs and receipts can contain local paths and licence-holder details; keep them private.

A zero polynomial-client exit and passed receipt require all stated checks. The diode command instead checks the exact retained failure and its separate transient criteria; it is not a successful reverse-current validation.

```text
& $Python -B .\run_models.py --device poly --josim $JoSIM --licence $Licence --expected-version 2.9.0 --output-root .\poly-review
if ($LASTEXITCODE -ne 0) { throw 'The polynomial teaching reference did not pass; inspect the new receipt and first diagnostic.' }
```

## Save poly.inc

```text
.MODEL PM POLY(I0=.1m I1=1m I2=-.4m I3=2m Q0=7p Q1=2p Q2=.4p Q3=3p)
```

## Save poly-op.cir

```text
* Synthetic poly current and charge teaching model
.include "poly.inc"
Y1 N 0 PM
V1 N 0 DC .2
.OPTION SEED=321 ANALYSIS=VOLTAGE INTEGRATION=BDF
.OP
.END
```

## Save poly-dc.cir

```text
* Synthetic poly current and charge teaching model
.include "poly.inc"
Y1 N 0 PM
V1 N 0 DC .1
.OPTION SEED=321 ANALYSIS=VOLTAGE INTEGRATION=BDF
.DC V1 -.1 .5 .1
.END
```

## Save poly-ac.cir

```text
* Synthetic poly current and charge teaching model
.include "poly.inc"
Y1 N 0 PM
V1 N 0 DC .2 AC 1 30
.OPTION SEED=321 ANALYSIS=VOLTAGE INTEGRATION=BDF
.AC LIN 3 1G 3G
.PRINT AC V(N) I(Y1)
.END
```

## Save poly-tran.cir

```text
* Synthetic poly current and charge teaching model
.include "poly.inc"
Y1 N 0 PM
V1 N 0 PWL(0 -.1 2n .5)
.OPTION SEED=321 ANALYSIS=VOLTAGE INTEGRATION=BDF
.TRAN 5p 2n 0 5p
.PRINT V(N) I(Y1)
.END
```

## Save run_models.py

```text
"""Check the supplied Windows diode or POLY teaching inputs with JoSIM-Pro."""
import argparse
import cmath
import csv
import hashlib
import io
import json
import math
import os
from pathlib import Path
import re
import subprocess
import sys

STOP = 2e-9
SLOPE = .6 / STOP
RUNTIME_BOLTZMANN = 1.38064852e-23
SI_BOLTZMANN = 1.380649e-23


def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def equation(kind, voltage, boltzmann=RUNTIME_BOLTZMANN):
    """Independent conduction current and differential charge for these inputs."""
    v = voltage
    if kind == 'poly':
        current = .0001 + .001 * v - .0004 * v**2 + .002 * v**3
        capacitance = 2e-12 + .8e-12 * v + 9e-12 * v**2
        conductance = .001 - .0008 * v + .006 * v**2
    else:
        # The checked runtime retains an older k_B. Report the exact-SI
        # comparison separately; this is not an accuracy or calibration waiver.
        vt = boltzmann * 300.15 / 1.602176634e-19
        current = 2e-12 * math.expm1(v / vt)
        conductance = 2e-12 * math.exp(v / vt) / vt
        # AREA=2 scales both conduction and charge. At FC*VJ, continue
        # depletion capacitance linearly with its boundary slope.
        if v < .35:
            depletion = 4e-12 / math.sqrt(1 - v / .7)
        else:
            at_boundary = 4e-12 / math.sqrt(.5)
            depletion = at_boundary * (1 + .5 * (v - .35) / (.7 * .5))
        capacitance = depletion + 1e-9 * conductance
    return current, conductance, capacitance


def table(path):
    text = path.read_text(encoding='utf-8-sig')
    metadata = None
    if text.startswith('# josim-ac-csv/1 '):
        first, text = text.split('\n', 1)
        metadata = json.loads(first[len('# josim-ac-csv/1 '):])
    rows = list(csv.reader(io.StringIO(text)))
    names = rows[0]
    data = [[float(value) for value in row] for row in rows[1:] if row]
    if not data or len(names) != len(set(names)) or any(
            len(row) != len(names) or not all(math.isfinite(x) for x in row) for row in data):
        raise ValueError('Missing, duplicate, ragged or nonfinite numerical data')
    return metadata, names, data


def close(actual, expected):
    return math.isclose(actual, expected, rel_tol=1e-9, abs_tol=1e-15)


def static_check(kind, path, swept):
    _, names, rows = table(path)
    device = 'D1' if kind == 'diode' else 'Y1'
    axes = [-.1 + .1 * i for i in range(7)] if swept else [.2]
    if len(rows) != len(axes):
        raise ValueError('Unexpected static sample count')
    errors, si_differences, comparisons = [], [], []
    for axis, row in zip(axes, rows):
        values = dict(zip(names, row))
        expected = equation(kind, axis)[0]
        ok = (close(values['V(N) (V)'], axis)
              and 0 <= values['OP.normalized_residual (1)'] <= 1)
        if swept:
            ok = ok and close(values['Sweep(V1) (V)'], axis)
        if not ok:
            raise ValueError('Static voltage, sweep coordinate or solver residual gate failed')
        errors.append(abs(values['I(' + device + ') (A)'] - expected))
        si_current = equation(kind, axis, SI_BOLTZMANN)[0]
        si_differences.append(abs(values['I(' + device + ') (A)'] - si_current))
        comparisons.append({'bias_V': axis, 'actual_current_A': values['I(' + device + ') (A)'],
                            'runtime_equation_A': expected, 'exact_SI_equation_A': si_current,
                            'normalized_residual': values['OP.normalized_residual (1)'],
                            'current_match': close(values['I(' + device + ') (A)'], expected),
                            'source_current_match': close(values['I(V1) (A)'], -expected)})
    return {'samples': len(rows), 'max_current_error_A': max(errors),
            'max_current_difference_from_exact_SI_A': max(si_differences),
            'comparisons': comparisons,
            'strict_current_reference_passed': all(row['current_match'] and row['source_current_match'] for row in comparisons),
            'reference': 'Explicit runtime k_B; exact-SI difference is separate' if kind == 'diode' else 'Polynomial coefficients'}


def ac_check(kind, path):
    metadata, names, rows = table(path)
    device = 'D1' if kind == 'diode' else 'Y1'
    expected_names = ['frequency (Hz)', 'Re(V(N)) (V)', 'Im(V(N)) (V)',
                      'Re(I(' + device + ')) (A)', 'Im(I(' + device + ')) (A)']
    if names != expected_names or len(rows) != 3 or metadata is None:
        raise ValueError('Unexpected AC schema or sample count')
    if (metadata.get('phasor_convention') != 'peak_exp_positive_j'
            or str(metadata.get('effective_seed')) != '321'):
        raise ValueError('Unexpected AC convention or seed')
    _, conductance, capacitance = equation(kind, .2)
    source = cmath.rect(1, math.pi / 6)
    errors = []
    for frequency, rv, iv, ri, ii in rows:
        expected = source * complex(conductance, 2 * math.pi * frequency * capacitance)
        errors.append(abs(complex(ri, ii) - expected))
        if abs(complex(rv, iv) - source) > 1e-12 or errors[-1] > 1e-11:
            raise ValueError('AC phasor differs from the bias derivative reference')
    if [row[0] for row in rows] != [1e9, 2e9, 3e9]:
        raise ValueError('AC frequency axis differs from the requested grid')
    return {'samples': 3, 'conductance_S': conductance, 'capacitance_F': capacitance,
            'max_current_phasor_error_A': max(errors)}


def transient_check(kind, path, step):
    _, names, rows = table(path)
    device = 'D1' if kind == 'diode' else 'Y1'
    if names != ['time', 'V(N)', 'I(' + device + ')'] or len(rows) != round(STOP / step) + 1:
        raise ValueError('Unexpected transient columns or sample count')
    if not close(rows[0][2], equation(kind, -.1)[0]):
        raise ValueError('The initial sample must contain the static initial current')
    errors, ratios = [], []
    for index, (time, voltage, current) in enumerate(rows):
        if abs(time - index * step) > 1e-20 or abs(voltage - (-.1 + SLOPE * time)) > 1e-12:
            raise ValueError('Unexpected transient time or imposed-voltage coordinate')
        # Initial charge history and the final ramp corner are distinct from
        # the differentiable interior used for the time-discretisation check.
        if index < 3 or index == len(rows) - 1:
            continue
        conduction, _, capacitance = equation(kind, voltage)
        expected = conduction + capacitance * SLOPE
        error = abs(current - expected)
        bound = 1e-8 if kind == 'poly' else 1e-8 + .001 * abs(expected)
        errors.append(error)
        ratios.append(error / bound)
    return {'samples': len(rows), 'interior_samples': len(errors), 'step_s': step,
            'max_current_error_A': max(errors), 'max_bound_ratio': max(ratios),
            'reference_bound_passed': max(ratios) <= 1}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--device', choices=['diode', 'poly'], required=True)
    parser.add_argument('--josim', type=Path, required=True)
    parser.add_argument('--licence', type=Path, required=True)
    parser.add_argument('--expected-version', required=True)
    parser.add_argument('--output-root', type=Path, required=True)
    args = parser.parse_args()
    source = Path(__file__).resolve().parent
    names = [args.device + '.inc'] + [args.device + '-' + mode + '.cir' for mode in ('op', 'dc', 'ac', 'tran')]
    try:
        if os.name != 'nt':
            raise ValueError('This checked launcher is Windows-specific')
        binary, licence = args.josim.resolve(strict=True), args.licence.resolve(strict=True)
        root = args.output_root.resolve()
        if not binary.is_file() or not licence.is_file() or root.exists():
            raise ValueError('Use existing executable/licence files and a new output root')
        if any(c in str(licence) for c in '\r\n'):
            raise ValueError('The licence path cannot contain a line break')
        licence_hash = sha(licence)
        inputs = {name: sha(source / name) for name in names}
        root.mkdir(parents=True, exist_ok=False)
    except (OSError, ValueError) as error:
        print('Preflight refused: ' + str(error), file=sys.stderr)
        return 2
    receipt = {'status': 'failed', 'device': args.device, 'inputs': inputs,
               'scope': 'Supplied synthetic Windows teaching inputs only; no measured-device calibration'}
    try:
        home = root / 'home'
        (home / '.josimpro').mkdir(parents=True)
        (home / '.josimpro/config.ini').write_text('[General]\nLicenseLocation=' + str(licence) + '\n', encoding='utf-8')
        environment = os.environ.copy()
        environment['USERPROFILE'] = str(home)
        environment['PATH'] = str(binary.parent) + os.pathsep + environment.get('PATH', '')
        for key, suffix in [('APPDATA', 'AppData/Roaming'), ('LOCALAPPDATA', 'AppData/Local')]:
            (home / suffix).mkdir(parents=True)
            environment[key] = str(home / suffix)
        version = subprocess.run([str(binary), '--version'], env=environment, capture_output=True, timeout=15)
        (root / 'private-version.log').write_bytes(version.stdout + version.stderr)
        found = re.search(r'(?m)^v(\d+\.\d+\.\d+)\s*$', (version.stdout + version.stderr).decode('utf-8', errors='replace'))
        if version.returncode or not found or found[1] != args.expected_version:
            raise ValueError('The resolved executable does not report the requested version')
        receipt['runtime'] = {'version': found[1], 'binary_sha256': sha(binary)}
        results = {}
        for mode in ('op', 'dc', 'ac', 'tran', 'coarse', 'refined'):
            directory = root / mode
            directory.mkdir()
            main_name = args.device + '-' + ('tran' if mode in {'coarse', 'refined'} else mode) + '.cir'
            text = (source / main_name).read_text(encoding='utf-8')
            if mode in {'coarse', 'refined'}:
                replacement = '.TRAN 20p 2n 0 20p' if mode == 'coarse' else '.TRAN 2.5p 2n 0 2.5p'
                if text.count('.TRAN 5p 2n 0 5p') != 1:
                    raise ValueError('The supplied teaching transient was changed')
                text = text.replace('.TRAN 5p 2n 0 5p', replacement)
            deck = directory / main_name
            deck.write_text(text, encoding='utf-8', newline='\n')
            (directory / (args.device + '.inc')).write_bytes((source / (args.device + '.inc')).read_bytes())
            output = directory / 'result.csv'
            run = subprocess.run([str(binary), '-P', '17', '-o', str(output), str(deck)],
                                 cwd=directory, env=environment, capture_output=True, timeout=60)
            (directory / 'private-process.log').write_bytes(run.stdout + run.stderr)
            if run.returncode != 0 or not output.is_file():
                raise ValueError(mode + ' did not produce a current successful analysis file')
            if mode in {'op', 'dc'}:
                result = static_check(args.device, output, mode == 'dc')
            elif mode == 'ac':
                result = ac_check(args.device, output)
            else:
                step = {'tran': 5e-12, 'coarse': 20e-12, 'refined': 2.5e-12}[mode]
                result = transient_check(args.device, output, step)
            result['output_sha256'] = sha(output)
            results[mode] = result
        receipt['results'] = results
        if not all(results[mode]['strict_current_reference_passed'] for mode in ('op', 'dc')):
            raise ValueError('Strict static current reference failed; inspect per-row deltas. Solver acceptance is separate from numerical accuracy')
        if (results['coarse']['reference_bound_passed'] or not results['tran']['reference_bound_passed']
                or not results['refined']['reference_bound_passed']
                or results['refined']['max_current_error_A'] >= .5 * results['tran']['max_current_error_A']):
            raise ValueError('The declared teaching time-refinement checks failed')
        if any(sha(source / name) != value for name, value in inputs.items()) or sha(licence) != licence_hash:
            raise ValueError('A source or issued licence changed during execution')
        receipt['status'] = 'passed'
        print(json.dumps({'status': 'passed', 'device': args.device,
                          'transient_current_error_A': results['tran']['max_current_error_A']}))
    except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
        receipt['reason'] = str(error)
        print('Check failed: ' + str(error), file=sys.stderr)
    finally:
        (root / 'run.json').write_text(json.dumps(receipt, indent=2) + '\n', encoding='utf-8')
    return 0 if receipt['status'] == 'passed' else 1


if __name__ == '__main__':
    raise SystemExit(main())
```

## Check the result

Require the intended saved inputs, actual compatible runtime, new artifacts, correct axes/units and the independent per-analysis verdict. Solver success, GUI freshness and numerical/physical acceptance remain separate. Retain both diode limitations as failed or qualified references.

## If the problem remains

Preserve the complete synthetic inputs, exact runtime and first diagnostic privately. Check units, temperature, supported mode and saved source before changing the model. Do not infer measured-device accuracy or a supported model feature from a finite plot alone.

## Sources and verification details

Recorded inputs/builds only. Complete walkthrough and final-package acceptance remain separate.

- VAL-068: Fresh frozen Windows JoSIM-Pro 2.9.0 nonlinear review candidate. 39 real CLI/client/reference checks, complete D/POLY inputs, OP/DC/complex AC, time refinement, unsupported-mode and model-domain rejection, actual nonlinear failure, protected prior files and new recovery. The diode strict reverse-current and exact-SI comparisons remain distinct failed/limited references. The actual browser-downloaded bytes repeat all 39 contract checks and pass ten exact-command/archive/verdict comparisons. This passed review records bounded successes and expected failures; it does not accept the failed diode current reference, arbitrary-model physical accuracy, stability/uniqueness, GUI, other platforms or final packages.
- VAL-069: Fresh frozen Windows JoSIM-Pro 2.9.0 nonlinear review candidate. 25 independent native/source/artifact checks, two normal app processes, fourteen successful runs and one failed run. Actual editor key/text commit, Cancel/No/Yes, source/disk separation, coarser-step/Undo/save recovery, typed AC opening and explicit saved-source reruns. No can run the saved model while the document incorrectly says Results current. No product repair, complete GUI article, physical OS picker/pointer, automatic session restoration, final package/platform or general measured-device accuracy acceptance. The task, document label and numerical reference retain separate scopes.

- josim-poly-dc-response: Fresh frozen Windows JoSIM-Pro 2.9.0 nonlinear candidate, reviewed 4 November 2026. Bounded illustration with independent raw-data checks. Known reference/freshness limits, physical OS picker/pointer, arbitrary physical model and final-package/platform qualification remain separate.
- josim-poly-transient-response: Fresh frozen Windows JoSIM-Pro 2.9.0 nonlinear candidate, reviewed 4 November 2026. Bounded illustration with independent raw-data checks. Known reference/freshness limits, physical OS picker/pointer, arbitrary physical model and final-package/platform qualification remain separate.
- josim-nonlinear-save-before-run: Fresh frozen Windows JoSIM-Pro 2.9.0 nonlinear candidate, reviewed 4 November 2026. Bounded illustration with independent raw-data checks. Known reference/freshness limits, physical OS picker/pointer, arbitrary physical model and final-package/platform qualification remain separate.

- josim-pro:docs/manual_source/sections/syntax.tex: Diode and polynomial current/charge models
- validation:nonlinear-models-20261104: VAL-068/069 complete synthetic cases, strict failed references, actual save choices and current artifacts

- 39 CLI/client/reference and 25 native/source/artifact checks on a freshly frozen Windows 2.9.0 candidate; installed products, settings and issued licences are preserved.
- The complete diode sweep retains its strict reverse-current failure; the exact-SI constant comparison is separate. Known issues are documented, not repaired.
- Complete GUI article, physical operator input, all nonlinear topologies, other platforms, final packages and measured-device calibration remain unqualified.

## Related guides

- [Run the intended saved netlist and recognise stale results](/knowledge-base/josim-run-current-source)
- [Run a static operating point or DC source sweep](/knowledge-base/josim-dc-op)
- [Run and interpret a complex small-signal AC analysis](/knowledge-base/josim-ac-analysis)
- [Simulate a diode and check current, charge and reference limits](/knowledge-base/josim-diode-current-charge)
