{"sections":[{"heading":"Use separate cases for the checked route","paragraphs":["The checked route creates a complete saved project for each length/width pair and invokes the normal licensed InductEx CLI. Studio can inspect each generated IXI and its results. The Python driver is supplied with the example; it is not a built-in Studio parameter-study feature.","The legacy SWEEP command needs particular care in the recorded 8.0.1 runtime. Valid single-variable/global requests returned exit 0 with “Sweep control moved to InductEx Studio after version 7.1.100”, but produced no extracted elements or sweep*_view.m file. The same valid variable request in the netlist .control block behaved that way. The warning does not prove that another GUI command ran the requested grid.","A wrong-direction legacy request warned that it could not parse the sweep, then completed one unswept nominal extraction with exit 0. A positive L1 from that run was not a parameter study. This is tracked as KB-CLI-001; use the explicit route below rather than accepting the process exit or one old result as evidence of a sweep."]},{"figures":[{"asset_id":"studio-inline-parameters","caption":"The existing native Raw IXI illustration shows the exact default strip_length = 100 and strip_width = 10 inputs used for the nominal case. It illustrates source parameters; it does not show a Studio batch-run control."}],"heading":"Prepare and inspect the complete example","steps":["Extract the complete four-file ZIP into a new writable folder. Keep strip.ixi, process.ldf, strip.cir and sweep_strip.py together. Use a matching licensed InductEx/TTH installation and standard-library Python 3.10 or later; Windows x64 CPython 3.13.11 was checked.","Open strip.ixi in Studio and inspect Raw IXI. strip_length and strip_width drive both the M1 polygon and terminal positions. The process Units = 1e-6 makes these dimensions micrometres. The default is 100 µm long and 10 µm wide.","Check the process and netlist references and close or save any independent editor changes before running the script. The driver reads the files beside itself; it does not execute unsaved Studio text.","Select an actual executable in $ix and the intended interpreter in $python below. The script uses a new case directory for every calculation, so no previous output can satisfy a new case."]},{"code":"$ix = 'C:\\Program Files\\SUN Magnetics\\InductEx\\bin\\inductex.exe'\n$python = 'python' # Select your compatible Python interpreter if different.\n& $python .\\sweep_strip.py --inductex $ix --results .\\study-001\nif ($LASTEXITCODE -ne 0) { throw 'Study failed; inspect the new generation before retrying.' }","heading":"Run the six-case Cartesian study","paragraphs":["Run these PowerShell commands from the extracted directory. The defaults select lengths 80, 100 and 120 µm and widths 8 and 10 µm. Width varies fastest: (80,8), (80,10), (100,8), (100,10), (120,8), (120,10). Each pair starts a separate extraction with no continuation state.","study-001 must not exist. Keep the terminal exit and inspect study.json after the script returns; its status must be passed and every expected case must be present. The script does not change the original three extraction inputs or global licence settings."]},{"heading":"Check the result and units","paragraphs":["study.csv is the driver’s numerical summary; study.json records statuses, requested dimensions, runtime/input identities and per-case elements hashes. Each case retains its own strip.ixi, process.ldf, strip.cir and output/elements.txt. The summary stores L1_H in henries; the table below displays picohenries.","In this fixed teaching model, longer lines have larger inductance and wider lines have smaller inductance. These are observed trends for this grid, not a calibration or universal monotonicity rule. The 100 × 10 µm case reproduces 3.896479063 pH against the 3.9 pH nominal design, about 0.0903% below it. Other geometries have different intended values; do not judge the whole grid against that one nominal target.","This is a geometry study at the supplied mesh settings. It does not establish mesh convergence, a physical uncertainty bound or a universal solver tolerance. Refine representative cases separately while preserving geometry, materials, terminals and units."],"table":{"columns":["Length (µm)","Width (µm)","Recorded L1 (pH)"],"rows":[["80","8","3.826498635"],["80","10","3.114709983"],["100","8","4.787794491"],["100","10","3.896479063"],["120","8","5.748008094"],["120","10","4.679059834"]],"wrap":true}},{"code":"& $python .\\sweep_strip.py --inductex $ix --results .\\nominal-001 --lengths 100 --widths 10\nif ($LASTEXITCODE -ne 0) { throw 'Nominal case failed; inspect its diagnostic.' }","heading":"Rerun only the nominal case","paragraphs":["The axis options accept explicit positive finite values, including a one-case selection. Choose a new generation name for each run. The teaching driver refuses duplicates and caps its own grid at 64 cases; that is a driver limit, not a claim about the product’s maximum supported study size."]},{"heading":"Inspect a generated case in Studio","paragraphs":["The batch script and its recovery were checked through the CLI. Opening/re-running every generated case through Studio is a separate pending GUI walkthrough; the reference image does not establish that execution."],"steps":["Open the intended case-NNN/strip.ixi, not just the original template. Confirm its requested length and width in Raw IXI and its local process/netlist references.","Read that case’s current output/elements.txt and diagnostic record alongside the driver’s row. If you run the case again from Studio, treat that as a separate execution and check the new task verdict and output identity.","Keep source and numerical output together when comparing cases. A mesh snapshot can explain geometry; it does not prove that the requested case solved. Use the related Results guide to distinguish current, stale and imported output."]},{"heading":"Recover without accepting a partial grid","paragraphs":["The driver refuses a legacy SWEEP directive in either teaching input before creating a result generation. Remove that command from the copied template and select the intended axes with --lengths and --widths instead. This guard prevents the legacy fallback from being mistaken for the explicit study.","An existing generation, duplicate/invalid axis or excessive case count is rejected before a new calculation begins. Choose a new result directory and correct the requested dimensions; never delete a previous generation just to make the command pass.","A failed extraction stops the driver with exit 1. Its study.json has status failed, the failed case has no accepted L1_H and unstarted_cases counts the remaining work. Earlier passed rows, if any, are only a completed prefix. A running receipt left by interruption is also not a complete study.","The checked invalid process failed at the first case and left the second unstarted. Restoring the complete supplied process and running into a new directory recovered the nominal result. Fix the first parser/licence/model/solver diagnostic before retrying.","diagnostic-private.txt and solver records can contain machine paths, licence-holder text or design details. Keep them local and review them before sharing a support record. No licence file is included in the teaching ZIP."]},{"code":"* Parameterised inline equivalent of the maintained microstrip geometry\n$COMMAND\n  MeshFile \"strip.msh\"\n  MeshType CUBE\n  Mode MQS\n  Netlist \"strip.cir\"\n  Process \"process.ldf\"\n$END\n$VARIABLE\n  Name strip_length\n  Value 100\n$END\n$VARIABLE\n  Name strip_width\n  Value 10\n$END\n$STRUCT\n  Name inline_strip\n  $POLY\n    Layer M1\n    XY\n    (0,0)\n    (0,%strip_width)\n    (%strip_length,%strip_width)\n    (%strip_length,0)\n    (0,0)\n  $END\n  $PATH\n    Layer TERM\n    Width 1\n    XY\n    (0,%strip_width)\n    (0,0)\n  $END\n  $PATH\n    Layer TERM\n    Width 1\n    XY\n    (%strip_length,%strip_width)\n    (%strip_length,0)\n  $END\n  $TEXT\n    Text \"P1 M1 M0\"\n    Layer TEXT\n    XY\n    (0,%strip_width/2)\n  $END\n  $TEXT\n    Text \"P2 M1 M0\"\n    Layer TEXT\n    XY\n    (%strip_length,%strip_width/2)\n  $END\n$END\n","heading":"Complete strip.ixi"},{"code":"* Public microstrip teaching stack; dimensions use Units below\n$Parameters\n  Units = 1e-6\n  SegmentSize = 2.0\n  AbsMin = 0.025\n  GPOverhang = 5.0\n  ProcessHasGroundPlane = TRUE\n  GPLayer = 17\n  TermLayer = 19\n  TextLayer = 18\n  HFilaments = 1\n  TerminalInRange = 1.0\n  UnitL = 1\n$End\n$Layer\n  Number = 17\n  Name = M0\n  Thickness = 0.2\n  Lambda = 0.09\n  Order = 0\n  Mask = -1\n  Filmtype = S\n  HFilaments = 2\n$End\n$Layer\n  Number = 2\n  Name = I0\n  Thickness = 0.15\n  Order = 1\n  Mask = -1\n  Filmtype = I\n$End\n$Layer\n  Number = 5\n  Name = M1\n  Thickness = 0.25\n  Lambda = 0.09\n  Order = 2\n  Mask = 1\n  Filmtype = S\n  HFilaments = 3\n$End\n","heading":"Complete process.ldf"},{"code":"* Extraction topology, design inductance in henries because UnitL=1\nL1 1 2 3.9e-12\nP1 1 0\nP2 2 0\n.end\n","heading":"Complete strip.cir"},{"code":"\"\"\"Run the supplied microstrip parameter grid as separate licensed extractions.\n\nStandard-library Python. Inputs beside this script remain unchanged. Each run\ngets a new folder; an existing result generation is refused. This is a teaching\ndriver, not an implementation of the legacy SWEEP control command.\n\"\"\"\nimport argparse\nimport csv\nimport hashlib\nimport itertools\nimport json\nimport math\nimport os\nfrom pathlib import Path\nimport re\nimport subprocess\nimport sys\n\n\ndef sha(path):\n    return hashlib.sha256(path.read_bytes()).hexdigest()\n\n\ndef positive(text):\n    value = float(text)\n    if not math.isfinite(value) or value <= 0:\n        raise argparse.ArgumentTypeError('Use a positive finite dimension in micrometres')\n    return value\n\n\ndef set_variable(text, name, value):\n    pattern = r'(\\$VARIABLE\\s+Name\\s+' + re.escape(name) + r'\\s+Value\\s+)([^\\s]+)'\n    result, count = re.subn(pattern, lambda m: m[1] + format(value, '.17g'), text, flags=re.I)\n    if count != 1:\n        raise ValueError('Expected exactly one supplied variable: ' + name)\n    return result\n\n\ndef main():\n    p = argparse.ArgumentParser(description=__doc__)\n    p.add_argument('--inductex', type=Path, required=True, help='Actual licensed inductex executable')\n    p.add_argument('--results', type=Path, required=True, help='New result directory; never reuse one')\n    p.add_argument('--lengths', nargs='+', type=positive, default=[80.0, 100.0, 120.0])\n    p.add_argument('--widths', nargs='+', type=positive, default=[8.0, 10.0])\n    args = p.parse_args()\n    binary = args.inductex.resolve()\n    if not binary.is_file():\n        p.error('The InductEx executable does not exist')\n    if len(args.lengths) * len(args.widths) > 64:\n        p.error('This teaching driver limits a request to 64 cases')\n    if len(set(args.lengths)) != len(args.lengths) or len(set(args.widths)) != len(args.widths):\n        p.error('Remove duplicate axis values')\n    inputs = Path(__file__).resolve().parent\n    names = ('strip.ixi', 'process.ldf', 'strip.cir')\n    if any(not (inputs / name).is_file() for name in names):\n        p.error('Keep all three supplied extraction inputs beside this script')\n    original = {name: (inputs / name).read_bytes() for name in names}\n    original_hashes = {name: sha(inputs / name) for name in names}\n    for name in ('strip.ixi', 'strip.cir'):\n        if re.search(r'^\\s*SWEEP\\b', original[name].decode('utf-8'), re.I | re.M):\n            p.error('Remove legacy SWEEP commands from the teaching inputs; select this driver\\'s explicit axes instead')\n    geometry = original['strip.ixi'].decode('utf-8')\n    # Validate the complete template before creating a generation.\n    set_variable(set_variable(geometry, 'strip_length', args.lengths[0]), 'strip_width', args.widths[0])\n    result_dir = args.results.resolve()\n    try:\n        result_dir.mkdir(parents=True, exist_ok=False)\n    except FileExistsError:\n        p.error('Result directory already exists; choose a new generation name')\n    env = os.environ.copy()\n    env['PATH'] = str(binary.parent) + os.pathsep + env.get('PATH', '')\n    cases = []\n    receipt = {'schema': 'sunmagnetics-teaching-sweep/1', 'status': 'running',\n               'binary_sha256': sha(binary), 'input_sha256': original_hashes,\n               'expected_cases': len(args.lengths) * len(args.widths), 'cases': cases}\n\n    def save():\n        (result_dir / 'study.json').write_text(json.dumps(receipt, indent=2) + '\\n', encoding='utf-8')\n        with (result_dir / 'study.csv').open('w', encoding='utf-8', newline='') as f:\n            writer = csv.DictWriter(f, fieldnames=['case', 'length_um', 'width_um', 'status', 'exit_code', 'L1_H'])\n            writer.writeheader()\n            writer.writerows({k: c.get(k) for k in writer.fieldnames} for c in cases)\n\n    save()\n    for index, (length, width) in enumerate(itertools.product(args.lengths, args.widths), 1):\n        folder = result_dir / ('case-' + str(index).zfill(3))\n        folder.mkdir()\n        for name, data in original.items():\n            (folder / name).write_bytes(data)\n        changed = set_variable(set_variable(geometry, 'strip_length', length), 'strip_width', width)\n        (folder / 'strip.ixi').write_text(changed, encoding='utf-8', newline='\\n')\n        case = {'case': index, 'length_um': length, 'width_um': width, 'status': 'running',\n                'exit_code': None, 'L1_H': None,\n                'input_sha256': {name: sha(folder / name) for name in names}}\n        cases.append(case)\n        save()\n        try:\n            run = subprocess.run([str(binary), 'strip.ixi'], cwd=folder, env=env,\n                                 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,\n                                 creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))\n            case['exit_code'] = run.returncode\n            # Keep this local: tool banners/diagnostics can contain paths or licence-holder details.\n            (folder / 'diagnostic-private.txt').write_bytes(run.stdout)\n            elements = folder / 'output/elements.txt'\n            match = re.search(r'^L1\\s+([-+\\d.eE]+)\\s*$', elements.read_text(encoding='utf-8-sig'), re.M) if elements.is_file() else None\n            value = float(match[1]) if match else None\n            if run.returncode != 0 or value is None or not math.isfinite(value) or value <= 0:\n                raise RuntimeError('The current case has no successful positive finite L1; inspect its local diagnostic')\n            case['L1_H'] = value\n            case['elements_sha256'] = sha(elements)\n            case['status'] = 'passed'\n        except (OSError, RuntimeError, ValueError, UnicodeError, subprocess.TimeoutExpired) as error:\n            case['status'] = 'failed'\n            case['error'] = type(error).__name__\n            if isinstance(error, subprocess.TimeoutExpired):\n                (folder / 'diagnostic-private.txt').write_bytes(error.stdout or b'')\n            receipt['status'] = 'failed'\n            receipt['unstarted_cases'] = receipt['expected_cases'] - len(cases)\n            save()\n            print('Case', index, 'failed; inspect the private case diagnostic and use a new result directory.', file=sys.stderr)\n            return 1\n        save()\n        print('Case', index, 'completed:', format(value * 1e12, '.9g'), 'pH')\n    preserved = all(sha(inputs / name) == digest for name, digest in original_hashes.items())\n    receipt['source_inputs_preserved'] = preserved\n    receipt['status'] = 'passed' if preserved else 'failed'\n    save()\n    return 0 if preserved else 1\n\n\nif __name__ == '__main__':\n    raise SystemExit(main())\n","heading":"Complete sweep_strip.py"}],"canonical_url":"https://www.sun-magnetics.com/knowledge-base/inductex-sweeps","url":"/knowledge-base/inductex-sweeps","known_issues":[{"disposition":"verified_workaround","id":"KB-CLI-001","title":"legacy SWEEP can exit zero without executing a study","public_notice":"Use the supplied external case driver with separate complete cases. Legacy SWEEP may exit successfully without executing a study; require every expected current result.","claim_scope":"Only the affected claim and recorded build/input; not whole-guide or final-release acceptance"}],"search_record":{"execution":true,"relatedProducts":[],"version_tags":["InductEx 8 preview"],"text":"Run an InductEx parameter study with separate checked cases Sweep a complete microstrip teaching model over length and width, retain each extraction and reject incomplete studies. Recognise the legacy SWEEP warning and its exit-zero trap. InductEx 8 beta/preview; Windows catalog 8.0.1 / TTH 1.1.50 scoped evidence How to sweep parameter study Cartesian length width strip_length strip_width SWEEP exits zero no output Sweep control moved independent cases batch extraction Use separate cases for the checked route The checked route creates a complete saved project for each length/width pair and invokes the normal licensed InductEx CLI. Studio can inspect each generated IXI and its results. The Python driver is supplied with the example; it is not a built-in Studio parameter-study feature. The legacy SWEEP command needs particular care in the recorded 8.0.1 runtime. Valid single-variable/global requests returned exit 0 with “Sweep control moved to InductEx Studio after version 7.1.100”, but produced no extracted elements or sweep*_view.m file. The same valid variable request in the netlist .control block behaved that way. The warning does not prove that another GUI command ran the requested grid. A wrong-direction legacy request warned that it could not parse the sweep, then completed one unswept nominal extraction with exit 0. A positive L1 from that run was not a parameter study. This is tracked as KB-CLI-001; use the explicit route below rather than accepting the process exit or one old result as evidence of a sweep.  Prepare and inspect the complete example Extract the complete four-file ZIP into a new writable folder. Keep strip.ixi, process.ldf, strip.cir and sweep_strip.py together. Use a matching licensed InductEx/TTH installation and standard-library Python 3.10 or later; Windows x64 CPython 3.13.11 was checked. Open strip.ixi in Studio and inspect Raw IXI. strip_length and strip_width drive both the M1 polygon and terminal positions. The process Units = 1e-6 makes these dimensions micrometres. The default is 100 µm long and 10 µm wide. Check the process and netlist references and close or save any independent editor changes before running the script. The driver reads the files beside itself; it does not execute unsaved Studio text. Select an actual executable in $ix and the intended interpreter in $python below. The script uses a new case directory for every calculation, so no previous output can satisfy a new case.  The existing native Raw IXI illustration shows the exact default strip_length = 100 and strip_width = 10 inputs used for the nominal case. It illustrates source parameters; it does not show a Studio batch-run control. Raw IXI shows the MQS project references, explicit strip_length and strip_width variables and the parameterised M1 polygon. Run the six-case Cartesian study Run these PowerShell commands from the extracted directory. The defaults select lengths 80, 100 and 120 µm and widths 8 and 10 µm. Width varies fastest: (80,8), (80,10), (100,8), (100,10), (120,8), (120,10). Each pair starts a separate extraction with no continuation state. study-001 must not exist. Keep the terminal exit and inspect study.json after the script returns; its status must be passed and every expected case must be present. The script does not change the original three extraction inputs or global licence settings. $ix = 'C:\\Program Files\\SUN Magnetics\\InductEx\\bin\\inductex.exe'\n$python = 'python' # Select your compatible Python interpreter if different.\n& $python .\\sweep_strip.py --inductex $ix --results .\\study-001\nif ($LASTEXITCODE -ne 0) { throw 'Study failed; inspect the new generation before retrying.' } Check the result and units study.csv is the driver’s numerical summary; study.json records statuses, requested dimensions, runtime/input identities and per-case elements hashes. Each case retains its own strip.ixi, process.ldf, strip.cir and output/elements.txt. The summary stores L1_H in henries; the table below displays picohenries. In this fixed teaching model, longer lines have larger inductance and wider lines have smaller inductance. These are observed trends for this grid, not a calibration or universal monotonicity rule. The 100 × 10 µm case reproduces 3.896479063 pH against the 3.9 pH nominal design, about 0.0903% below it. Other geometries have different intended values; do not judge the whole grid against that one nominal target. This is a geometry study at the supplied mesh settings. It does not establish mesh convergence, a physical uncertainty bound or a universal solver tolerance. Refine representative cases separately while preserving geometry, materials, terminals and units.  Length (µm) Width (µm) Recorded L1 (pH) 80 8 3.826498635 80 10 3.114709983 100 8 4.787794491 100 10 3.896479063 120 8 5.748008094 120 10 4.679059834 Rerun only the nominal case The axis options accept explicit positive finite values, including a one-case selection. Choose a new generation name for each run. The teaching driver refuses duplicates and caps its own grid at 64 cases; that is a driver limit, not a claim about the product’s maximum supported study size. & $python .\\sweep_strip.py --inductex $ix --results .\\nominal-001 --lengths 100 --widths 10\nif ($LASTEXITCODE -ne 0) { throw 'Nominal case failed; inspect its diagnostic.' } Inspect a generated case in Studio The batch script and its recovery were checked through the CLI. Opening/re-running every generated case through Studio is a separate pending GUI walkthrough; the reference image does not establish that execution. Open the intended case-NNN/strip.ixi, not just the original template. Confirm its requested length and width in Raw IXI and its local process/netlist references. Read that case’s current output/elements.txt and diagnostic record alongside the driver’s row. If you run the case again from Studio, treat that as a separate execution and check the new task verdict and output identity. Keep source and numerical output together when comparing cases. A mesh snapshot can explain geometry; it does not prove that the requested case solved. Use the related Results guide to distinguish current, stale and imported output.  Recover without accepting a partial grid The driver refuses a legacy SWEEP directive in either teaching input before creating a result generation. Remove that command from the copied template and select the intended axes with --lengths and --widths instead. This guard prevents the legacy fallback from being mistaken for the explicit study. An existing generation, duplicate/invalid axis or excessive case count is rejected before a new calculation begins. Choose a new result directory and correct the requested dimensions; never delete a previous generation just to make the command pass. A failed extraction stops the driver with exit 1. Its study.json has status failed, the failed case has no accepted L1_H and unstarted_cases counts the remaining work. Earlier passed rows, if any, are only a completed prefix. A running receipt left by interruption is also not a complete study. The checked invalid process failed at the first case and left the second unstarted. Restoring the complete supplied process and running into a new directory recovered the nominal result. Fix the first parser/licence/model/solver diagnostic before retrying. diagnostic-private.txt and solver records can contain machine paths, licence-holder text or design details. Keep them local and review them before sharing a support record. No licence file is included in the teaching ZIP.  Complete strip.ixi * Parameterised inline equivalent of the maintained microstrip geometry\n$COMMAND\n  MeshFile \"strip.msh\"\n  MeshType CUBE\n  Mode MQS\n  Netlist \"strip.cir\"\n  Process \"process.ldf\"\n$END\n$VARIABLE\n  Name strip_length\n  Value 100\n$END\n$VARIABLE\n  Name strip_width\n  Value 10\n$END\n$STRUCT\n  Name inline_strip\n  $POLY\n    Layer M1\n    XY\n    (0,0)\n    (0,%strip_width)\n    (%strip_length,%strip_width)\n    (%strip_length,0)\n    (0,0)\n  $END\n  $PATH\n    Layer TERM\n    Width 1\n    XY\n    (0,%strip_width)\n    (0,0)\n  $END\n  $PATH\n    Layer TERM\n    Width 1\n    XY\n    (%strip_length,%strip_width)\n    (%strip_length,0)\n  $END\n  $TEXT\n    Text \"P1 M1 M0\"\n    Layer TEXT\n    XY\n    (0,%strip_width/2)\n  $END\n  $TEXT\n    Text \"P2 M1 M0\"\n    Layer TEXT\n    XY\n    (%strip_length,%strip_width/2)\n  $END\n$END\n Complete process.ldf * Public microstrip teaching stack; dimensions use Units below\n$Parameters\n  Units = 1e-6\n  SegmentSize = 2.0\n  AbsMin = 0.025\n  GPOverhang = 5.0\n  ProcessHasGroundPlane = TRUE\n  GPLayer = 17\n  TermLayer = 19\n  TextLayer = 18\n  HFilaments = 1\n  TerminalInRange = 1.0\n  UnitL = 1\n$End\n$Layer\n  Number = 17\n  Name = M0\n  Thickness = 0.2\n  Lambda = 0.09\n  Order = 0\n  Mask = -1\n  Filmtype = S\n  HFilaments = 2\n$End\n$Layer\n  Number = 2\n  Name = I0\n  Thickness = 0.15\n  Order = 1\n  Mask = -1\n  Filmtype = I\n$End\n$Layer\n  Number = 5\n  Name = M1\n  Thickness = 0.25\n  Lambda = 0.09\n  Order = 2\n  Mask = 1\n  Filmtype = S\n  HFilaments = 3\n$End\n Complete strip.cir * Extraction topology, design inductance in henries because UnitL=1\nL1 1 2 3.9e-12\nP1 1 0\nP2 2 0\n.end\n Complete sweep_strip.py \"\"\"Run the supplied microstrip parameter grid as separate licensed extractions.\n\nStandard-library Python. Inputs beside this script remain unchanged. Each run\ngets a new folder; an existing result generation is refused. This is a teaching\ndriver, not an implementation of the legacy SWEEP control command.\n\"\"\"\nimport argparse\nimport csv\nimport hashlib\nimport itertools\nimport json\nimport math\nimport os\nfrom pathlib import Path\nimport re\nimport subprocess\nimport sys\n\n\ndef sha(path):\n    return hashlib.sha256(path.read_bytes()).hexdigest()\n\n\ndef positive(text):\n    value = float(text)\n    if not math.isfinite(value) or value <= 0:\n        raise argparse.ArgumentTypeError('Use a positive finite dimension in micrometres')\n    return value\n\n\ndef set_variable(text, name, value):\n    pattern = r'(\\$VARIABLE\\s+Name\\s+' + re.escape(name) + r'\\s+Value\\s+)([^\\s]+)'\n    result, count = re.subn(pattern, lambda m: m[1] + format(value, '.17g'), text, flags=re.I)\n    if count != 1:\n        raise ValueError('Expected exactly one supplied variable: ' + name)\n    return result\n\n\ndef main():\n    p = argparse.ArgumentParser(description=__doc__)\n    p.add_argument('--inductex', type=Path, required=True, help='Actual licensed inductex executable')\n    p.add_argument('--results', type=Path, required=True, help='New result directory; never reuse one')\n    p.add_argument('--lengths', nargs='+', type=positive, default=[80.0, 100.0, 120.0])\n    p.add_argument('--widths', nargs='+', type=positive, default=[8.0, 10.0])\n    args = p.parse_args()\n    binary = args.inductex.resolve()\n    if not binary.is_file():\n        p.error('The InductEx executable does not exist')\n    if len(args.lengths) * len(args.widths) > 64:\n        p.error('This teaching driver limits a request to 64 cases')\n    if len(set(args.lengths)) != len(args.lengths) or len(set(args.widths)) != len(args.widths):\n        p.error('Remove duplicate axis values')\n    inputs = Path(__file__).resolve().parent\n    names = ('strip.ixi', 'process.ldf', 'strip.cir')\n    if any(not (inputs / name).is_file() for name in names):\n        p.error('Keep all three supplied extraction inputs beside this script')\n    original = {name: (inputs / name).read_bytes() for name in names}\n    original_hashes = {name: sha(inputs / name) for name in names}\n    for name in ('strip.ixi', 'strip.cir'):\n        if re.search(r'^\\s*SWEEP\\b', original[name].decode('utf-8'), re.I | re.M):\n            p.error('Remove legacy SWEEP commands from the teaching inputs; select this driver\\'s explicit axes instead')\n    geometry = original['strip.ixi'].decode('utf-8')\n    # Validate the complete template before creating a generation.\n    set_variable(set_variable(geometry, 'strip_length', args.lengths[0]), 'strip_width', args.widths[0])\n    result_dir = args.results.resolve()\n    try:\n        result_dir.mkdir(parents=True, exist_ok=False)\n    except FileExistsError:\n        p.error('Result directory already exists; choose a new generation name')\n    env = os.environ.copy()\n    env['PATH'] = str(binary.parent) + os.pathsep + env.get('PATH', '')\n    cases = []\n    receipt = {'schema': 'sunmagnetics-teaching-sweep/1', 'status': 'running',\n               'binary_sha256': sha(binary), 'input_sha256': original_hashes,\n               'expected_cases': len(args.lengths) * len(args.widths), 'cases': cases}\n\n    def save():\n        (result_dir / 'study.json').write_text(json.dumps(receipt, indent=2) + '\\n', encoding='utf-8')\n        with (result_dir / 'study.csv').open('w', encoding='utf-8', newline='') as f:\n            writer = csv.DictWriter(f, fieldnames=['case', 'length_um', 'width_um', 'status', 'exit_code', 'L1_H'])\n            writer.writeheader()\n            writer.writerows({k: c.get(k) for k in writer.fieldnames} for c in cases)\n\n    save()\n    for index, (length, width) in enumerate(itertools.product(args.lengths, args.widths), 1):\n        folder = result_dir / ('case-' + str(index).zfill(3))\n        folder.mkdir()\n        for name, data in original.items():\n            (folder / name).write_bytes(data)\n        changed = set_variable(set_variable(geometry, 'strip_length', length), 'strip_width', width)\n        (folder / 'strip.ixi').write_text(changed, encoding='utf-8', newline='\\n')\n        case = {'case': index, 'length_um': length, 'width_um': width, 'status': 'running',\n                'exit_code': None, 'L1_H': None,\n                'input_sha256': {name: sha(folder / name) for name in names}}\n        cases.append(case)\n        save()\n        try:\n            run = subprocess.run([str(binary), 'strip.ixi'], cwd=folder, env=env,\n                                 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,\n                                 creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))\n            case['exit_code'] = run.returncode\n            # Keep this local: tool banners/diagnostics can contain paths or licence-holder details.\n            (folder / 'diagnostic-private.txt').write_bytes(run.stdout)\n            elements = folder / 'output/elements.txt'\n            match = re.search(r'^L1\\s+([-+\\d.eE]+)\\s*$', elements.read_text(encoding='utf-8-sig'), re.M) if elements.is_file() else None\n            value = float(match[1]) if match else None\n            if run.returncode != 0 or value is None or not math.isfinite(value) or value <= 0:\n                raise RuntimeError('The current case has no successful positive finite L1; inspect its local diagnostic')\n            case['L1_H'] = value\n            case['elements_sha256'] = sha(elements)\n            case['status'] = 'passed'\n        except (OSError, RuntimeError, ValueError, UnicodeError, subprocess.TimeoutExpired) as error:\n            case['status'] = 'failed'\n            case['error'] = type(error).__name__\n            if isinstance(error, subprocess.TimeoutExpired):\n                (folder / 'diagnostic-private.txt').write_bytes(error.stdout or b'')\n            receipt['status'] = 'failed'\n            receipt['unstarted_cases'] = receipt['expected_cases'] - len(cases)\n            save()\n            print('Case', index, 'failed; inspect the private case diagnostic and use a new result directory.', file=sys.stderr)\n            return 1\n        save()\n        print('Case', index, 'completed:', format(value * 1e12, '.9g'), 'pH')\n    preserved = all(sha(inputs / name) == digest for name, digest in original_hashes.items())\n    receipt['source_inputs_preserved'] = preserved\n    receipt['status'] = 'passed' if preserved else 'failed'\n    save()\n    return 0 if preserved else 1\n\n\nif __name__ == '__main__':\n    raise SystemExit(main())\n","id":"inductex-sweeps","keywords":"sweep parameter study Cartesian length width strip_length strip_width SWEEP exits zero no output Sweep control moved independent cases batch extraction","kind":"How to","title":"Run an InductEx parameter study with separate checked cases","product":"InductEx"},"version":"8 beta/preview; Windows catalog 8.0.1 / TTH 1.1.50 scoped evidence","id":"inductex-sweeps","related":["/knowledge-base/inductex-ixi-geometry","/knowledge-base/inductex-netlist-build","/knowledge-base/inductex-mesh-convergence","/knowledge-base/inductex-read-results"],"downloads":[{"url":"/_functions/kbExample?name=inductex-strip-study.zip","scope":"Exact checked input files plus instructions; final-release/platform applicability remains pending.","files":["strip.ixi","process.ldf","strip.cir","sweep_strip.py","README.txt"],"id":"inductex-strip-study","requires":["Licensed matching InductEx/TTH installation","Windows x64 Python 3.13 checked; standard library only","Extract every member into a writable directory and choose a new result generation"],"title":"Complete microstrip length/width study"}],"publication_status":"published","numerical_output":null,"provenance":{"execution_checks":[{"routes":["python-cli"],"scope":"Twenty-six checks on a licensed Windows 8.0.1/TTH 1.1.50 runtime: six independent MQS cases, unchanged input bytes, per-case CSV/JSON results, guarded requests, failure/recovery and reproduced legacy SWEEP limitations. No built-in Studio sweep, mesh convergence, physical calibration or final-package acceptance","result":"passed","articles":["inductex-sweeps"],"purpose":"inductex-independent-parameter-study-and-legacy-sweep-limit","version":"Windows catalog InductEx 8.0.1 and bundled TTH 1.1.50, 3 November 2026; not final-package acceptance","id":"VAL-031","records":["complete four-file teaching input","six-case grid and single-case result","invalid/reused request and process recovery","legacy IXI and netlist control observations","Legacy SWEEP rejected in both teaching input entry paths before case creation","Seven actual-download checks replay both exact PowerShell command blocks from the final guarded ZIP"],"limit":"The teaching driver runs separate cases sequentially and checks fresh positive finite L1 values. Exit zero alone did not establish a legacy sweep. GUI parameter-study orchestration and other devices/materials/platforms remain unqualified.","observed":{"cases":[{"L1_H":3.826498635e-12,"case":1,"length_um":80,"width_um":8},{"L1_H":3.114709983e-12,"case":2,"length_um":80,"width_um":10},{"L1_H":4.787794491e-12,"case":3,"length_um":100,"width_um":8},{"L1_H":3.896479063e-12,"case":4,"length_um":100,"width_um":10},{"L1_H":5.748008094e-12,"case":5,"length_um":120,"width_um":8},{"L1_H":4.679059834e-12,"case":6,"length_um":120,"width_um":10}],"legacy":[{"elements_present":false,"moved_warning":true,"direction_warning":false,"exit_code":0,"legacy_sweep_outputs":[],"case":"legacy-variable","L1_H":null},{"elements_present":false,"moved_warning":true,"direction_warning":false,"exit_code":0,"legacy_sweep_outputs":[],"case":"legacy-global","L1_H":null},{"elements_present":true,"moved_warning":false,"direction_warning":true,"exit_code":0,"legacy_sweep_outputs":[],"case":"legacy-wrong-direction","L1_H":3.896479063e-12},{"elements_present":false,"moved_warning":true,"direction_warning":false,"exit_code":0,"legacy_sweep_outputs":[],"case":"legacy-netlist","L1_H":null}]}}],"figure_evidence":[{"evidence_limit":"Authoring illustration only; no GUI extraction or complete walkthrough is shown. CLI/editor results are recorded separately. Annotation alias and .end warnings remain under review.","id":"studio-inline-parameters","reference_label":"Current Windows Studio source, native authoring illustration, 3 November 2026"}],"review_notes":["Twenty-six bounded real CLI/driver checks; source inputs and existing result generations preserved. Runtime identities are retained in immutable evidence.","Legacy SWEEP is not presented as a working current feature. A warning about Studio is not proof of a GUI sweep implementation.","One existing native authoring illustration is reused for the exact default template. Complete generated-case GUI walkthrough and final package/platform checks remain pending."],"scope":"Recorded inputs/builds only. Complete walkthrough and final-package acceptance remain separate.","verification_requirements":{"gui_method":"Product-native generated-case walkthrough remains pending; current figure is a source illustration","status":"review-incomplete","illustrations":"required","required":["technical-review","release-applicability","execution","recovery","numerical-interpretation","gui-workflow","illustrations"],"legacy_walkthrough_field":"pending retained; not a complete GUI article"},"sources":[{"id":"inductex:docs/manual/ix_netlist.tex","section":"Legacy SWEEP forms; current limitation qualified by VAL-031"},{"id":"inductex:docs/manual/ix_layoutinput.tex","section":"Named IXI variables, expressions and inline geometry"},{"id":"inductex:docs/manual/ix_ldf.tex","section":"Dimensions and result unit conventions"},{"id":"capture:studio-inputs-20261103T163127","section":"Exact default input native Raw IXI illustration only"},{"id":"validation:inductex-sweeps-windows-20261103","section":"VAL-031; independent-case grid, guarded generation, recovery and legacy SWEEP observations"}]},"title":"Run an InductEx® parameter study with separate checked cases","type":"How to","markdown_url":"/_functions/kbResource?path=markdown%2Finductex-sweeps.md","summary":"Sweep a complete microstrip teaching model over length and width, retain each extraction and reject incomplete studies. Recognise the legacy SWEEP warning and its exit-zero trap.","product":"InductEx"}