#!/usr/bin/env python
# -------------------------------------------------------------------------------
#
# ███████╗██████╗ ██╗ ██████╗███████╗██╗ ██╗██████╗
# ██╔════╝██╔══██╗██║██╔════╝██╔════╝██║ ██║██╔══██╗
# ███████╗██████╔╝██║██║ █████╗ ██║ ██║██████╔╝
# ╚════██║██╔═══╝ ██║██║ ██╔══╝ ██║ ██║██╔══██╗
# ███████║██║ ██║╚██████╗███████╗███████╗██║██████╔╝
# ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝╚═════╝
#
# Name: simulator.py
# Purpose: Creates a virtual class for representing all Spice Simulators
#
# Author: Nuno Brum (nuno.brum@gmail.com)
#
# Created: 23-12-2016
# Licence: refer to the LICENSE file
# -------------------------------------------------------------------------------
import sys
from abc import ABC, abstractmethod
from pathlib import Path, PureWindowsPath
import subprocess
import os
import logging
import shutil
import shlex
_logger = logging.getLogger("spicelib.Simulator")
def run_function(command, timeout=None, stdout=None, stderr=None, cwd=None):
"""Normalizing OS subprocess function calls between different platforms."""
_logger.debug(f"Running command: {command}, with timeout: {timeout}")
result = subprocess.run(command, timeout=timeout, stdout=stdout, stderr=stderr, cwd=cwd)
return result.returncode
class SpiceSimulatorError(Exception):
"""Generic Simulator Error Exceptions"""
...
[docs]
class Simulator(ABC):
"""Pure static class template for Spice simulators. This class only defines the interface of the subclasses.
The variables below shall be overridden by the subclasses. Instantiating this class will raise a SpiceSimulatorError
exception.
A typical subclass for a Windows installation is:
.. code-block:: python
class MySpiceWindowsInstallation(Simulator):
spice_exe = ['<path to the spice executable>']
process_name = "<name of the process on Windows Task Manager>"
or on a Linux distribution:
.. code-block:: python
class MySpiceLinuxInstallation(Simulator):
spice_exe = ['<wine_command>', '<path to the spice executable>']
process_name = "<name of the process within the system>"
If you use MacOS, you can choose either one of the 2 above. If you are on Intel, running LTSpice under wine (therefore: like under Linux) is preferred.
The subclasses should then implement at least the run() function as a classmethod.
.. code-block:: python
@classmethod
def run(cls, netlist_file: str | Path, cmd_line_switches: list | None = None, timeout: float | None = None,
stdout=None, stderr=None, cwd: str | Path | None = None, exe_log: bool = False) -> int:
'''This method implements the call for the simulation of the netlist file. '''
cmd_run = cls.spice_exe + ['-Run'] + ['-b'] + [netlist_file] + cmd_line_switches
return run_function(cmd_run, timeout=timeout, stdout=stdout, stderr=stderr, cwd=cwd)
The ``run_function()`` can be imported from the simulator.py with
``from spicelib.sim.simulator import run_function`` instruction.
"""
spice_exe: list[str] = []
""" The executable. If using a loader (like wine), make sure that the last in the array is the real simulator.
:meta hide-value:"""
process_name: str = ""
""" the name of the process in the task manager
:meta hide-value:"""
raw_extension = '.raw'
""":meta private:"""
# the default lib paths, as used by get_default_library_paths
_default_lib_paths = []
[docs]
@classmethod
def create_from(cls, path_to_exe, process_name=None):
"""
Creates a simulator class from a path to the simulator executable
:param path_to_exe:
:type path_to_exe: pathlib.Path or str. If it is a string, it supports multiple sections,
allowing loaders like wine, but MUST be in posix format in that case, and
the last section MUST be the simulator executable.
:param process_name: the process_name to be used for killing phantom processes. If not provided, it will be
:return: a class instance representing the Spice simulator
:rtype: Simulator
"""
plib_path_to_exe = None
exe_parts = []
if isinstance(path_to_exe, Path) or os.path.exists(path_to_exe):
if isinstance(path_to_exe, Path):
plib_path_to_exe = path_to_exe
else:
plib_path_to_exe = Path(path_to_exe)
exe_parts = [plib_path_to_exe.as_posix()]
else:
if '\\' in path_to_exe: # this probably a windows path. Don't be smart here.
# make the path into a posix path. Rather complicated gymnastics, but it works.
# I do not support multiple sections here, as it is not likely needed.
plib_path_to_exe = Path(PureWindowsPath(path_to_exe).as_posix())
exe_parts = [plib_path_to_exe.as_posix()]
else:
# try to extract the parts
exe_parts = shlex.split(path_to_exe)
if len(exe_parts) > 0:
plib_path_to_exe = Path(exe_parts[0])
exe_parts[0] = plib_path_to_exe.as_posix()
if plib_path_to_exe is not None and (plib_path_to_exe.exists() or shutil.which(plib_path_to_exe)):
if process_name is None:
cls.process_name = cls.guess_process_name(exe_parts[0])
else:
cls.process_name = process_name
cls.spice_exe = exe_parts
return cls
else:
raise FileNotFoundError(f"Provided exe file was not found '{path_to_exe}'")
[docs]
@staticmethod
def guess_process_name(exe: str) -> str:
"""Guess the process name based on the executable path"""
if not exe:
return ""
if sys.platform == 'darwin':
if "wine" in exe:
# For MacOS wine, there will be no process called "wine". Use "wine-preloader"
return "wine-preloader"
else:
return Path(exe).stem
else:
return Path(exe).name
def __init__(self):
raise SpiceSimulatorError("This class is not supposed to be instanced.")
[docs]
@classmethod
@abstractmethod
def run(cls, netlist_file: str | Path, cmd_line_switches: list | None = None, timeout: float | None= None,
stdout=None, stderr=None, cwd: str | Path | None = None, exe_log: bool = False) -> int:
"""This method implements the call for the simulation of the netlist file. This should be overriden by its
subclass."""
raise SpiceSimulatorError("This class should be subclassed and this function should be overridden.")
[docs]
@classmethod
@abstractmethod
def valid_switch(cls, switch, switch_param) -> list:
"""This method validates that a switch exist and is valid. This should be overriden by its subclass."""
...
[docs]
@classmethod
def is_available(cls):
"""This method checks if the simulator exists in the system. It will return a boolean value indicating if the
simulator is installed or not."""
if cls.spice_exe and len(cls.spice_exe) > 0:
# check if file exists
if Path(cls.spice_exe[0]).exists():
return True
# check if file in path
if shutil.which(cls.spice_exe[0]):
return True
return False
[docs]
@classmethod
def get_default_library_paths(cls) -> list[str]:
"""
Return the directories that contain the standard simulator's libraries,
as derived from the simulator's executable path and platform.
spice_exe must be set before calling this method.
This is companion with `set_custom_library_paths()`
:return: the list of paths where the libraries should be located.
"""
paths = []
myexe = None
# get the executable
if cls.spice_exe and len(cls.spice_exe) > 0:
# TODO: this will fail if the simulator executable is not in the last element of the list. Maybe make this more robust.
if os.path.exists(cls.spice_exe[-1]):
myexe = cls.spice_exe[-1]
_logger.debug(f"Using Spice executable path '{myexe}' to determine the correct library paths.")
for path in cls._default_lib_paths:
_logger.debug(f"Checking if library path '{path}' exists.")
p = cls.expand_and_check_local_dir(path, myexe)
if p is not None:
_logger.debug(f"Adding path '{p}' to the library path list")
paths.append(p)
return paths
[docs]
@staticmethod
def expand_and_check_local_dir(path: str, exe_path: str = None) -> str | None:
"""
Expands a directory path to become an absolute path, while taking into account a potential use under wine (under MacOS and Linux).
Will also check if that directory exists.
The path must either be an absolute path or start with ~. Relative paths are not supported.
On MacOS or Linux, it will try to replace any reference to the virtual windows root under wine into a host OS path.
Examples:
* under windows:
* C:/mydir -> C:/mydir
* ~/mydir -> C:/Users/myuser/mydir
* under linux, and if the executable is /mywineroot/.wine/drive_c/(something):
* C:/mydir -> /mywineroot/.wine/drive_c/mydir
* ~/mydir -> /mywineroot/.wine/drive_c/users/myuser/mydir
:param path: The path to expand. Must be in posix format, use `PureWindowsPath(path).as_posix()` to transform a windows path to a posix path.
:param exe_path: path to a related executable that may or may not be under wine, defaults to None, ignored on Windows
:return: the fully expanded path, as posix path, will return None if the path does not exist.
"""
c_drive = None
# See if I'm under wine
if sys.platform == "linux" or sys.platform == "darwin":
if exe_path and "/drive_c/" in exe_path:
# this is very likely a wine path
c_drive = exe_path.split("/drive_c/")[0] + "/drive_c/"
# if so: Translate C drive to the wine root
if c_drive is not None:
# this must be linux or darwin, with wine
if path.startswith("~"):
# Normally, a large number of directories in the home directory of a user under wine are symlinked
# to the user's home directory in the host OS. That would mean, that "~/Documents" under wine is
# normally also "~/Documents" under the host OS. But this is not always the case, and not for all directories.
# The user can have modified this, via for example a winetricks sandbox.
# Therefore, I make it an absolute path for Windows and do not try to optimise:
path = "C:/users/" + os.path.expandvars("${USER}" + path[1:])
# If I were to do this expansion under Windows, I should use ${USERNAME} but we're not in Windows here.
# I also cannot use expanduser(), as that again would be for the wrong OS.
# All lowercase "users" is correct, as it is the default path for the user's home directory in wine.
# I now have a "windows" path (but in posix form, with forward slashes). Make it into a host OS path.
if path.startswith("C:/") or path.startswith("c:/"):
path = c_drive + path[3:] # should start with C:. If not, something is wrong.
# note that in theory, the exe path can be relative to the user's home directory, so...
# and in all cases (Windows, MacOS, linux,...):
# terminate with the expansion of the ~
if path.startswith("~"):
path = os.path.expanduser(path)
# check existance and if it is a directory
if os.path.exists(path) and os.path.isdir(path):
return path
return None