macOS: resolve device-model libs (validated on real Apple Silicon)
Hardware testing on a native Apple Silicon Mac (LTspice XVII 17.2.4) surfaced
that model/subcircuit-bearing circuits failed with 'Unable to find definition
of model' / 'Could not open library file': native Mac LTspice in batch mode
does NOT auto-include the standard device libs or search the lib dir the way
Wine does (Wine netlists the .asc itself on Linux, so this never showed there).
Two fixes:
- netlister.py: emit .lib standard.{bjt,mos,dio,jft} for each semiconductor
family present, since Mac LTspice won't auto-include them in batch mode.
- runner.py: _resolve_lib_paths() rewrites bare-name .lib/.include refs to
absolute paths under the Mac lib root before running -b (Mac LTspice can't
find them by name). darwin-only; Linux path untouched.
- test_netlister.py: skip the Wine-oracle comparison class when wine is absent
(it's a Linux-only oracle).
Validated on hardware: full suite 534 passed / 8 skipped on native macOS
(all 10 integration circuits simulate, incl. op-amp + BJT); Linux unchanged
(524 unit + 18 integration). Also answered Kevin's open questions on hardware:
#2 -- 'LTspice -b <netlist>' DOES run over bare SSH (Background session), so
mcltspice is fully headless on Mac since it always netlists itself; #3 --
examples are not pre-extracted on Mac (examples_exist reports false).
This commit is contained in:
parent
37caaf0310
commit
47beacc9e1
@ -93,6 +93,15 @@ _UNIVERSAL_OPAMP_PARAMS = (
|
||||
)
|
||||
_UNIVERSAL_OPAMP_LIB = "UniversalOpAmp2.lib"
|
||||
|
||||
# Standard device-model library per SPICE prefix letter. Mac LTspice does not
|
||||
# auto-include these in batch mode the way Wine does, so the netlister emits them.
|
||||
_STD_DEVICE_LIB: dict[str, str] = {
|
||||
"Q": "standard.bjt",
|
||||
"M": "standard.mos",
|
||||
"D": "standard.dio",
|
||||
"J": "standard.jft",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Pin:
|
||||
@ -363,6 +372,16 @@ def asc_to_netlist(asc_text: str) -> str:
|
||||
_emit_component(comp, pins_by_comp[idx], coord_to_net, lib_files)
|
||||
)
|
||||
|
||||
# macOS LTspice (unlike Wine) does not auto-include the standard device
|
||||
# model libraries in batch mode, so emit a .lib for each semiconductor
|
||||
# family present (2N2222 lives in standard.bjt, etc.). The netlister is
|
||||
# macOS-only -- on Linux, Wine netlists the .asc itself -- and LTspice
|
||||
# tolerates an unused .lib, so this is harmless.
|
||||
for nc in netlist.components:
|
||||
lib = _STD_DEVICE_LIB.get(nc.name[:1].upper())
|
||||
if lib:
|
||||
lib_files.add(lib)
|
||||
|
||||
for lib in sorted(lib_files):
|
||||
netlist.add_lib(lib)
|
||||
|
||||
|
||||
@ -118,6 +118,43 @@ async def _run_and_wait(
|
||||
)
|
||||
|
||||
|
||||
def _resolve_lib_paths(net_text: str) -> str:
|
||||
"""Rewrite bare-name .lib/.include references to absolute paths (macOS).
|
||||
|
||||
Wine LTspice resolves an unqualified ``.lib standard.bjt`` via its configured
|
||||
search path; native Mac LTspice in batch mode does not, so an unqualified
|
||||
reference fails with "Could not open library file". We resolve each bare
|
||||
filename to its absolute location under the lib tree (first rglob match).
|
||||
Already-absolute paths and references with a path separator are left as-is.
|
||||
"""
|
||||
from .config import LTSPICE_LIB
|
||||
|
||||
if not LTSPICE_LIB.exists():
|
||||
return net_text
|
||||
|
||||
cache: dict[str, str | None] = {}
|
||||
|
||||
def _find(name: str) -> str | None:
|
||||
if name not in cache:
|
||||
matches = sorted(LTSPICE_LIB.rglob(name))
|
||||
cache[name] = str(matches[0]) if matches else None
|
||||
return cache[name]
|
||||
|
||||
out: list[str] = []
|
||||
for line in net_text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.lower().startswith((".lib ", ".include ", ".inc ")):
|
||||
kw, _, arg = stripped.partition(" ")
|
||||
arg = arg.strip().strip('"')
|
||||
if arg and "/" not in arg and not Path(arg).is_absolute():
|
||||
abs_path = _find(arg)
|
||||
if abs_path:
|
||||
out.append(f'{kw} "{abs_path}"')
|
||||
continue
|
||||
out.append(line)
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def _safe_copy(src: Path, dst: Path) -> None:
|
||||
"""Copy src to dst, skipping when they are the same file.
|
||||
|
||||
@ -224,6 +261,7 @@ async def run_simulation(
|
||||
from .netlister import asc_to_netlist # lazy, darwin-only
|
||||
|
||||
net_text = asc_to_netlist(work_schematic.read_text(errors="replace"))
|
||||
net_text = _resolve_lib_paths(net_text)
|
||||
run_target = work_schematic.with_suffix(".net")
|
||||
run_target.write_text(net_text)
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ synthesized N00x names are cosmetic and don't match LTspice's numbering).
|
||||
"""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections import defaultdict
|
||||
@ -321,6 +322,10 @@ def _wine_netlist(asc_text: str) -> str | None:
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.skipif(
|
||||
shutil.which("wine") is None,
|
||||
reason="Wine -netlist oracle is Linux-only; not available on native macOS",
|
||||
)
|
||||
class TestNetlisterAgainstWineOracle:
|
||||
"""asc_to_netlist topology must match Wine's native netlister.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user