Compatibility
8.0.2 isolated Windows CLI review candidate; final package applicability pending
Package applicability is under review; follow the stated version and platform limits.
Download Complete Windows teaching extraction and receipt checker (ZIP)Includes strip.ixi, process.ldf, strip.cir, run_extraction.py, README.txt. Exact checked input files plus instructions; final-release/platform applicability remains pending.

Use the current IXI-first invocation

A normal invocation is the intended InductEx® executable followed by the saved project. --version and --build-info are separate identity queries. Resolve the executable explicitly; a familiar command name or matching filename does not identify the binary actually selected.

The maintained parameter reference restores legacy compatibility and retains an older transition description. That older statement that all simulation flags are rejected is not the current contract. Keep new project settings in IXI and use the actual --help output for the selected release.

The checked runtime is a fresh isolated current-source Windows 8.0.2 CLI paired with the installed licensed TTH engine. No installed product, licence setting, version metadata or public download catalog was changed. Other platforms and final packages retain their own qualification.

Prepare the complete teaching input

  1. Extract strip.ixi, process.ldf, strip.cir and run_extraction.py together into a new writable folder. Keep their exact names; the IXI uses inline geometry and needs no GDS.
  2. Configure the existing issued InductEx® licence through the normal product settings. Set $Python to the Python 3 interpreter, $InductEx® to the compatible CLI executable and $TTH to its compatible TTH executable, using full paths.
  3. Use the expected-version value for the intended reviewed runtime. The checked command below requires 8.0.2 and deliberately refuses a different version before executing the project.
  4. Use a new run-first directory. Reusing an existing destination is refused before metadata queries or extraction, preserving the prior run and avoiding stale output.

Run the complete teaching extraction

Run this from the extracted directory. The helper changes only its child-process environment, copies the three source inputs into run-first/project and executes there. Input references are relative to that saved project; the outer caller can use another working directory.

Expect run.json, identity-query logs, process.log and current project/output/sol.txt, elements.txt and solver_out.txt. Raw logs can contain local paths or licence-holder information: keep them private and review them before sharing.

The supplied checker is for this synthetic MQS teaching model and UnitL=1. It requires the 3.9e-12 H design, a finite positive L1, agreement between detailed and rounded summary values, and less than 5% design deviation. The exercised detailed result is 3.896479063e-12 H; sol.txt rounds it to 3.89648e-12 H. This is a fixed-input design comparison, not mesh convergence, calibration or a generic validator for arbitrary circuits.

& $Python -B .\run_extraction.py --inductex $InductEx --tth $TTH --expected-version 8.0.2 --results .\run-first
if ($LASTEXITCODE -ne 0) { throw 'Extraction was refused or failed; inspect the current receipt and first diagnostic.' }

Read the current receipt and its limits

Accept the teaching run only when the runner returns zero, run.json says passed, process.state is exited, process_exit is zero, no controlled/error/warning condition is unresolved, and every required current artifact matches its recorded hash. The receipt retains source hashes, actual binary/solver hashes, version/build date and result units.

Job finished or the existence of sol.txt is insufficient. The tested missing-process and missing-netlist failures return process status 1 and can still write a partial sol.txt. The failed rerun tested here removed old elements.txt; neither that partial file nor an old receipt is accepted as a new result.

Valid legacy SWEEP can return process status 0 with a migration warning and no extracted elements (KB-CLI-001). The teaching helper refuses such requests before writing results; use the dedicated complete external study runner for parameter studies rather than infer a study from process success.

Handle timeouts and correct the cause

The Windows helper streams the log to disk and assigns its launched process to a kill-on-close job. Its subsequent descendants remain owned by that job. On timeout it records timed-out, terminates the owned job and returns failure; it does not treat a numerical exit value as proof of completed work.

The real timeout check recorded four owned processes before termination and independently confirmed that all were terminal afterwards. This is one Windows teaching workflow, not cross-platform cancellation qualification or authority to stop unrelated application/daemon processes.

For a failed input reference, read the first current diagnostic and restore the intended dependency or deliberately correct the IXI path. Correcting the teaching netlist reference and using a new results directory reproduces the original detailed L1 exactly. Preserve failed attempts and keep their outputs separate from recovery.

A negative/nonfinite timeout, wrong UnitL, missing source or legacy SWEEP request is a preflight refusal. A version mismatch writes a failed identity receipt before any project calculation. Consult the linked diagnostic guide for process status versus printed error numbers.

Save strip.ixi

* Parameterised inline equivalent of the maintained microstrip geometry
$COMMAND
  MeshFile "strip.msh"
  MeshType CUBE
  Mode MQS
  Netlist "strip.cir"
  Process "process.ldf"
$END
$VARIABLE
  Name strip_length
  Value 100
$END
$VARIABLE
  Name strip_width
  Value 10
$END
$STRUCT
  Name inline_strip
  $POLY
    Layer M1
    XY
    (0,0)
    (0,%strip_width)
    (%strip_length,%strip_width)
    (%strip_length,0)
    (0,0)
  $END
  $PATH
    Layer TERM
    Width 1
    XY
    (0,%strip_width)
    (0,0)
  $END
  $PATH
    Layer TERM
    Width 1
    XY
    (%strip_length,%strip_width)
    (%strip_length,0)
  $END
  $TEXT
    Text "P1 M1 M0"
    Layer TEXT
    XY
    (0,%strip_width/2)
  $END
  $TEXT
    Text "P2 M1 M0"
    Layer TEXT
    XY
    (%strip_length,%strip_width/2)
  $END
$END

Save process.ldf

* Public microstrip teaching stack; dimensions use Units below
$Parameters
  Units = 1e-6
  SegmentSize = 2.0
  AbsMin = 0.025
  GPOverhang = 5.0
  ProcessHasGroundPlane = TRUE
  GPLayer = 17
  TermLayer = 19
  TextLayer = 18
  HFilaments = 1
  TerminalInRange = 1.0
  UnitL = 1
$End
$Layer
  Number = 17
  Name = M0
  Thickness = 0.2
  Lambda = 0.09
  Order = 0
  Mask = -1
  Filmtype = S
  HFilaments = 2
$End
$Layer
  Number = 2
  Name = I0
  Thickness = 0.15
  Order = 1
  Mask = -1
  Filmtype = I
$End
$Layer
  Number = 5
  Name = M1
  Thickness = 0.25
  Lambda = 0.09
  Order = 2
  Mask = 1
  Filmtype = S
  HFilaments = 3
$End

Save strip.cir

* Extraction topology, design inductance in henries because UnitL=1
L1 1 2 3.9e-12
P1 1 0
P2 2 0
.end

Save run_extraction.py

"""Run the supplied Windows teaching extraction in a fresh owned directory."""
import argparse
import hashlib
import json
import math
import os
from pathlib import Path
import re
import subprocess
import sys

class ProcessJob:
    """Keep the launched Windows process and subsequent descendants in one job."""
    def __init__(self, process):
        self.handle = None
        if os.name != "nt":
            return
        import ctypes as C
        from ctypes import wintypes as W
        class BasicLimits(C.Structure):
            _fields_ = [("process_time", C.c_longlong), ("job_time", C.c_longlong),
                        ("flags", W.DWORD), ("minimum", C.c_size_t), ("maximum", C.c_size_t),
                        ("active", W.DWORD), ("affinity", C.c_size_t),
                        ("priority", W.DWORD), ("scheduling", W.DWORD)]
        class IoCounters(C.Structure):
            _fields_ = [(name, C.c_ulonglong) for name in
                        ("read_ops", "write_ops", "other_ops", "read_bytes", "write_bytes", "other_bytes")]
        class ExtendedLimits(C.Structure):
            _fields_ = [("basic", BasicLimits), ("io", IoCounters), ("process_memory", C.c_size_t),
                        ("job_memory", C.c_size_t), ("peak_process", C.c_size_t), ("peak_job", C.c_size_t)]
        self.api = C.WinDLL("kernel32", use_last_error=True)
        self.api.CreateJobObjectW.argtypes = [C.c_void_p, W.LPCWSTR]
        self.api.CreateJobObjectW.restype = W.HANDLE
        self.api.SetInformationJobObject.argtypes = [W.HANDLE, C.c_int, C.c_void_p, W.DWORD]
        self.api.AssignProcessToJobObject.argtypes = [W.HANDLE, W.HANDLE]
        self.api.CloseHandle.argtypes = [W.HANDLE]
        self.handle = self.api.CreateJobObjectW(None, None)
        limits = ExtendedLimits()
        limits.basic.flags = 0x2000  # JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
        if not self.handle or not self.api.SetInformationJobObject(self.handle, 9, C.byref(limits), C.sizeof(limits)):
            self.close()
            process.kill()
            raise OSError("Could not configure the child process watchdog job")
        if not self.api.AssignProcessToJobObject(self.handle, int(process._handle)):
            # A very short-lived child can exit before assignment.
            if process.poll() is None:
                self.close()
                process.kill()
                raise OSError("Could not assign the child process to its watchdog job")

    def close(self):
        if self.handle:
            self.api.CloseHandle(self.handle)
            self.handle = None

    def pids(self):
        import ctypes as C
        from ctypes import wintypes as W
        class ProcessIds(C.Structure):
            _fields_ = [('assigned', W.DWORD), ('count', W.DWORD), ('values', C.c_size_t * 64)]
        data = ProcessIds()
        self.api.QueryInformationJobObject.argtypes = [W.HANDLE, C.c_int, C.c_void_p, W.DWORD, C.c_void_p]
        if not self.api.QueryInformationJobObject(self.handle, 3, C.byref(data), C.sizeof(data), None):
            raise OSError('Could not inspect owned processes before timeout cleanup')
        return list(data.values[:data.count])


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


def write(path, value):
    path.write_text(json.dumps(value, indent=2) + '\n', encoding='utf-8')


def run_owned(command, cwd, log, environment, timeout):
    # Stream the raw log to disk. A Windows job owns the launched process and
    # descendants after assignment, and closes on every success/failure path.
    with log.open('wb') as stream:
        process = subprocess.Popen(command, cwd=cwd, env=environment,
            stdout=stream, stderr=subprocess.STDOUT, creationflags=subprocess.CREATE_NO_WINDOW)
        try:
            job = ProcessJob(process)
        except OSError:
            if process.poll() is None:
                process.kill()
            process.wait(timeout=10)
            raise
        try:
            try:
                code = process.wait(timeout=timeout)
                return {'state': 'exited', 'process_exit': code}
            except subprocess.TimeoutExpired:
                owned = job.pids()
                job.close()
                process.wait(timeout=10)
                return {'state': 'timed-out', 'process_exit': process.returncode, 'owned_pids_at_timeout': owned}
        finally:
            job.close()


def inspect(project, process, log):
    diagnostic = log.read_text(encoding='utf-8', errors='replace')
    codes = [int(c) for c in re.findall(r'(?m)^\((\d+)\) ', diagnostic)]
    result = {**process, 'controlled_diagnostic_codes': codes}
    if process['state'] != 'exited' or process['process_exit'] != 0:
        raise ValueError('The extraction process did not complete successfully')
    if codes or re.search(r'(?mi)^(?:ToolEngine error:|UNHANDLED EXCEPTION:|Not successful\.|ERRORS:|WARNINGS:|\s*ERROR\b|\s*WARNING\b)', diagnostic):
        raise ValueError('The current log contains a controlled diagnostic, error or warning requiring review')
    if 'Sweep control moved to InductEx Studio' in diagnostic:
        raise ValueError('Legacy SWEEP did not execute the requested study')
    sol, elements = project / 'output/sol.txt', project / 'output/elements.txt'
    if not sol.is_file() or not elements.is_file() or not (project / 'output/solver_out.txt').is_file():
        raise ValueError('Required fresh teaching artifacts are missing')
    t = sol.read_text(encoding='utf-8-sig', errors='strict')
    table = t.split('Inductance [H]', 1)[-1]
    row = re.search(r'^L1\s+([\d.eE+-]+)\s+([\d.eE+-]+)\s+', table, re.M)
    scalar = re.fullmatch(r'\s*L1\s+([\d.eE+-]+)\s*', elements.read_text(encoding='utf-8-sig'))
    if 'Inductance [H]' not in t or not row or not scalar:
        raise ValueError('Expected one mapped L1 teaching result in henries')
    design, extracted = map(float, row.groups())
    precise = float(scalar[1])
    if not all(math.isfinite(v) and v > 0 for v in (design, extracted, precise)):
        raise ValueError('Nonfinite or nonpositive teaching values')
    if not math.isclose(design, 3.9e-12, rel_tol=1e-12, abs_tol=0):
        raise ValueError('The teaching design value or result units changed')
    if float(format(precise, '.6g')) != extracted:
        raise ValueError('Current detailed and summary L1 values disagree')
    deviation = abs(precise - design) / design
    if deviation >= 0.05:
        raise ValueError('The teaching design comparison exceeds the stated 5 percent bound')
    result.update(design_H=design, extracted_H=precise,
                  design_deviation_percent=100 * deviation,
                  numerical_scope='Fixed teaching-input design comparison; not convergence or physical calibration')
    return result


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('--inductex', required=True, type=Path)
    p.add_argument('--tth', required=True, type=Path)
    p.add_argument('--expected-version', required=True)
    p.add_argument('--results', required=True, type=Path)
    p.add_argument('--timeout', type=float, default=60)
    a = p.parse_args()
    if sys.platform != 'win32' or not math.isfinite(a.timeout) or a.timeout <= 0:
        p.error('This checked runner requires Windows and a finite positive timeout')
    source = Path(__file__).resolve().parent
    names = ('strip.ixi', 'process.ldf', 'strip.cir')
    try:
        binary, solver = a.inductex.resolve(strict=True), a.tth.resolve(strict=True)
        if not binary.is_file() or not solver.is_file():
            raise ValueError('Executable paths must be files')
        inputs = {n: sha(source / n) for n in names}
        for n in ('strip.ixi', 'strip.cir'):
            if re.search(r'(?mi)^\s*SWEEP\b', (source / n).read_text(encoding='utf-8-sig')):
                raise ValueError('Use the dedicated study runner for legacy SWEEP requests')
        if not re.search(r'(?mi)^\s*UnitL\s*=\s*1\s*$', (source / 'process.ldf').read_text(encoding='utf-8-sig')):
            raise ValueError('This teaching runner requires UnitL=1, with results in henries')
        root = a.results.resolve()
        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', 'inputs': inputs,
               'scope': 'Supplied Windows synthetic MQS teaching model only',
               'licence_configuration_modified': False}
    try:
        environment = os.environ.copy()
        environment['TTH_PATH'] = str(solver)
        environment['PATH'] = str(binary.parent) + os.pathsep + str(solver.parent) + os.pathsep + environment.get('PATH', '')
        identities = {}
        for option, name in [('--version', 'version'), ('--build-info', 'build-info')]:
            run = run_owned([str(binary), option], root, root / (name + '.log'), environment, 10)
            if run['state'] != 'exited' or run['process_exit'] != 0:
                raise ValueError('Executable identity query failed')
            identities[name] = (root / (name + '.log')).read_text(encoding='utf-8-sig').strip()
        build = json.loads(identities['build-info'])
        if build['product_version'] != a.expected_version or identities['version'] != 'InductEx v' + a.expected_version:
            raise ValueError('Resolved executable version differs from the requested version')
        receipt['runtime'] = {'product_version': build['product_version'], 'build_date': build['build_date'],
                              'binary_sha256': sha(binary), 'solver_sha256': sha(solver)}
        project = root / 'project'
        project.mkdir()
        for n in names:
            (project / n).write_bytes((source / n).read_bytes())
        log = root / 'process.log'
        run = run_owned([str(binary), str(project / 'strip.ixi')], project, log, environment, a.timeout)
        raw = log.read_text(encoding='utf-8', errors='replace')
        receipt['process'] = {**run, 'controlled_diagnostic_codes': [int(c) for c in re.findall(r'(?m)^\((\d+)\) ', raw)]}
        receipt['result'] = inspect(project, run, log)
        if any(sha(source / n) != h or sha(project / n) != h for n, h in inputs.items()):
            raise ValueError('An input changed during execution')
        receipt['artifacts'] = {n: sha(project / 'output' / n) for n in ('sol.txt', 'elements.txt', 'solver_out.txt')}
        receipt['status'] = 'passed'
        write(root / 'run.json', receipt)
        print(json.dumps({'status': 'passed', 'version': build['product_version'], 'L1_H': receipt['result']['extracted_H']}))
        return 0
    except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
        receipt['reason'] = str(error)
        write(root / 'run.json', receipt)
        print(json.dumps({'status': 'failed', 'reason': str(error)}), file=sys.stderr)
        return 1


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

Check the result

Inspect the fresh receipt, first diagnostic, artifact identities and stated teaching reference. For another circuit, define its complete dependencies, requested quantities/units and independent numerical acceptance before adapting this helper. A source copy and process success alone do not establish physical correctness.

If the problem remains

Resolve the intended executable/solver and first current failure. Preserve the complete inputs and private log, correct a working copy and rerun into a new destination. Do not substitute a process code into the controlled diagnostic reference or reuse failed/stale artifacts.

Sources and verification details

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

VAL-067 · Fresh isolated Windows current-source InductEx® 8.0.2 CLI and installed licensed TTH
Frozen Windows InductEx® 8.0.2 CLI and real installed licensed TTH, complete teaching runner, actual process/controlled-diagnostic distinctions, partial/stale artifact rejection and real owned-process timeout/recovery. No GUI, general arbitrary-model/convergence, final package or other platform acceptance.
No GUI, arbitrary-model validator, general convergence/accuracy, every documented code, other platform or final-package qualification. A printed diagnostic code and process result are distinct.

  • inductex:docs/manual/ix_parameters.tex · Current InductEx® 8 behaviour
  • inductex:docs/manual/studio_results.tex · Read the run result
  • validation:cli-workflow-20261104 · VAL-067 complete teaching client, fresh artifacts, real failures and timeout/recovery
  • Actual fresh-source Windows CLI and real licensed TTH; installed products, issued licence/configuration and source version metadata are preserved. No final package/platform qualification.
  • 30 bounded checks retain CLI rejection, engine failure, printed diagnostics, partial sol.txt, actual timeout and correction. The complete reference preserves 68 active manual entries with explicit unexercised/legacy scope.
Read MarkdownRead structured articleBrowse all guides