Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SPICE tools #439

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions src/hammer-vlsi/hammer_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from decimal import Decimal

from .verilog_utils import *
from .spice_utils import *
from .lef_utils import *


Expand Down
140 changes: 140 additions & 0 deletions src/hammer-vlsi/hammer_utils/spice_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# spice_utils.py
# Misc Verilog utilities
jwright6323 marked this conversation as resolved.
Show resolved Hide resolved
#
# See LICENSE for licence details.

import re
from typing import Tuple, Optional, List, Set, Dict
from enum import Enum

class SpiceUtils:

# Multilines in spice start with a + character
multiline_pattern = re.compile("(?:\s*\n\+)+\s*", flags=re.DOTALL)
jwright6323 marked this conversation as resolved.
Show resolved Hide resolved
module_def_pattern = re.compile("^(\.subckt\s+)(?P<dname>[^\s]+)(\s+.*)$", flags=re.IGNORECASE|re.MULTILINE)
module_inst_pattern = re.compile("^(x.*?\s)(?P<mname>[^\s]+)(\s*)$", flags=re.IGNORECASE|re.MULTILINE)
end_module_pattern = re.compile("^\.ends.*$", flags=re.IGNORECASE|re.MULTILINE)
tokens = [
('SUBCKT', module_def_pattern.pattern),
('ENDS', end_module_pattern.pattern),
('INST', module_inst_pattern.pattern)
]
token_regex = re.compile('|'.join('(?P<%s>%s)' % t for t in tokens), flags=re.IGNORECASE|re.MULTILINE)

@staticmethod
def uniquify_spice(sources: List[str]) -> List[str]:
"""
Uniquify the provided SPICE sources. If a module name exists in multiple files, each duplicate module will be
renamed. If the original name of a duplicated module is used in a file where it is not defined, an
exception is thrown as it is impossible to know which renamed module to use. For example:

- file1.sp defines B and A, which instantiates B
- file2.sp defines B and C, which instantiates B

B would be renamed to B_0 in file1.sp
B would be renamed to B_1 in file2.sp

However, if file3.sp defines D, which instantiates B, an exception would be raised because we don't know if we
should chose B_0 or B_1.

:param sources: A list of the contents of SPICE source files (not the filenames)
:return: A list of SPICE sources with unique modules
"""
sources_no_multiline = [SpiceUtils.remove_multilines(s) for s in sources]
jwright6323 marked this conversation as resolved.
Show resolved Hide resolved
module_trees = [SpiceUtils.parse_module_tree(s) for s in sources_no_multiline]
found_modules = set() # type: Set[str]
duplicates = set() # type: Set[str]
extmods = set() # type: Set[str]
for tree in module_trees:
extmods.update(set(item for sublist in tree.values() for item in sublist if item not in set(tree.keys())))
for module in tree:
if module in found_modules:
duplicates.add(module)
found_modules.add(module)

invalid_extmods = extmods.intersection(duplicates)
if len(invalid_extmods) != 0:
raise ValueError("Unable to resolve master for duplicate SPICE module name: {}".format(",".join(invalid_extmods)))

def replace_source(source: str) -> str:
replacements = {} # type: Dict[str, str]
for old in duplicates:
i = 0
new = old
while new in found_modules:
new = "{d}_{i}".format(d=old, i=i)
i = i + 1
found_modules.add(new)
replacements[old] = new

return SpiceUtils.replace_modules(source, replacements)

return [replace_source(s) for s in sources_no_multiline]


@staticmethod
def parse_module_tree(s: str) -> Dict[str, Set[str]]:
"""
Parse a SPICE file and return a dictionary that contains all found modules pointing to lists of their submodules.
The SPICE file must not contain multiline statements.

:param s: A SPICE file without any multiline statements
:return: A dictionary whose keys are all found modules and whose values are the list of submodules
"""
in_module = False
module_name = ""
tree = {} # type: Dict[str, Set[str]]
for m in SpiceUtils.token_regex.finditer(s):
kind = m.lastgroup
if kind == 'SUBCKT':
module_name = m.group("dname")
in_module = True
if module_name in tree:
raise ValueError("Multiple SPICE subckt definitions for \"{}\" in the same file".format(module_name))
tree[module_name] = set()
elif kind == 'ENDS':
in_module = False
elif kind == 'INST':
if not in_module:
raise ValueError("Malformed SPICE source while parsing: \"{}\"".format(m.group()))
tree[module_name].add(m.group("mname"))
else:
assert False, "Should not get here"

return tree

@staticmethod
def replace_modules(source: str, mapping: Dict[str, str]) -> str:
"""
Replace module names in a provided SPICE file by the provided mapping.
The SPICE file must not contain multiline statements.


:param source: The input SPICE source without any multiline statements
:param mapping: A dictionary of old module names mapped to new module names
:return: SPICE source with the module names replaced
"""
# Not giving m a type because its type is different in different python3 versions :(
def repl_fn(m) -> str:
if m.group(2) in mapping:
return m.group(1) + mapping[m.group(2)] + m.group(3)
else:
return m.group(0)

return SpiceUtils.module_inst_pattern.sub(repl_fn, SpiceUtils.module_def_pattern.sub(repl_fn, source))

@staticmethod
def remove_multilines(s: str) -> str:
"""
Remove all multiline statements from the given SPICE source.

:param s: The SPICE source
:return: SPICE source without multiline statements
"""
return SpiceUtils.multiline_pattern.sub(" ", s)



7 changes: 2 additions & 5 deletions src/hammer-vlsi/hammer_utils/verilog_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@

import re

__all__ = ['VerilogUtils']


class VerilogUtils:
@staticmethod
def remove_comments(v: str) -> str:
"""
Remove comments from the given Verilog file.
Remove comments from the given Verilog source.

:param v: Verilog source code
:return: Source code without comments
Expand Down Expand Up @@ -46,7 +43,7 @@ def contains_module(v: str, module: str) -> bool:
@staticmethod
def remove_module(v: str, module: str) -> str:
"""
Remove the given module from the given Verilog source file, if it exists.
Remove the given module from the given Verilog source, if it exists.

:param v: Verilog source code
:param module: Module to remove
Expand Down
Loading