diff --git a/Changelog.md b/Changelog.md index 36a9e5b..d2f9d29 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,12 @@ +# 2.4.6 + +## Common + +- Improve layout detection + - Fix `.gds.gz` handling + - Unify codepaths for layout detection + - Unify log messages during regeneration + # 2.4.5 ## Common diff --git a/cace/__version__.py b/cace/__version__.py index c98aca2..0590425 100644 --- a/cace/__version__.py +++ b/cace/__version__.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '2.4.5' +__version__ = '2.4.6' if __name__ == '__main__': print(__version__, end='') diff --git a/cace/common/cace_regenerate.py b/cace/common/cace_regenerate.py index 7d4efb5..78c0226 100755 --- a/cace/common/cace_regenerate.py +++ b/cace/common/cace_regenerate.py @@ -24,7 +24,14 @@ from datetime import date as datetime import subprocess -from .common import get_pdk, get_pdk_root, get_magic_rcfile, set_xschem_paths +from .common import ( + get_pdk, + get_pdk_root, + get_magic_rcfile, + set_xschem_paths, + get_layout_path, + run_subprocess, +) from .misc import mkdirp from ..logging import ( @@ -259,205 +266,17 @@ def check_schematic_out_of_date(spicepath, schempath, debug=False): return need_capture -def regenerate_rcx_netlist(datasheet, runtime_options): - """Regenerate the R-C parasitic extracted netlist if out-of-date or if forced.""" - - debug = runtime_options['debug'] - force_regenerate = runtime_options['force'] - - dname = datasheet['name'] - netlistname = dname + '.spice' - vlogname = dname + '.v' - magicname = dname + '.mag' - gdsname = dname + '.gds' - xschemname = dname + '.sch' - - paths = datasheet['paths'] - - # Check the "paths" dictionary for paths to various files - - # Root path - if 'root' in paths: - root_path = paths['root'] - else: - root_path = '.' - - # Magic layout - if 'magic' in paths: - magicpath = paths['magic'] - magicfilename = os.path.join(magicpath, magicname) - else: - magicpath = None - magicfilename = None - - # GDS layout - if 'layout' in paths: - gdspath = paths['layout'] - gdsfilename = os.path.join(gdspath, gdsname) - else: - gdspath = None - gdsfilename = None - - # Schematic-captured netlist - if 'netlist' in paths: - schem_netlist_path = os.path.join(paths['netlist'], 'schematic') - schem_netlist = os.path.join(schem_netlist_path, netlistname) - else: - schem_netlist_path = None - schem_netlist = None - - # Layout-extracted netlist with R-C parasitics - if 'netlist' in paths: - rcx_netlist_path = os.path.join(paths['netlist'], 'rcx') - rcx_netlist = os.path.join(rcx_netlist_path, netlistname) - else: - rcx_netlist = None - rcx_netlist = None - - need_rcx_extraction = True - - if force_regenerate: - need_rcx_extract = True - else: - dbg('Checking for out-of-date RCX netlists.') - valid_layoutpath = magicfilename if magicpath else gdsfilename - need_rcx_extract = check_layout_out_of_date( - rcx_netlist, valid_layoutpath, debug - ) - - if need_rcx_extract: - dbg('Forcing regeneration of parasitic-extracted netlist.') - - if need_rcx_extract: - # Layout parasitic netlist needs regenerating. Check for magic layout. - - if (not magicfilename or not os.path.isfile(magicfilename)) and ( - not gdsfilename or not os.path.isfile(gdsfilename) - ): - err(f'Error: No netlist or layout for project {dname}. ') - if magicfilename: - err(f'(layout master file {magicfilename} not found.)\n') - else: - err(f'(layout master file {gdsfilename} not found.)\n') - return False - - # Check for parasitic netlist directory - if not os.path.exists(rcx_netlist_path): - os.makedirs(rcx_netlist_path) - - rcfile = get_magic_rcfile() - newenv = os.environ.copy() - - if 'PDK_ROOT' in datasheet: - pdk_root = datasheet['PDK_ROOT'] - else: - pdk_root = get_pdk_root() - - if 'PDK' in datasheet: - pdk = datasheet['PDK'] - else: - pdk = get_pdk(magicfilename) - - if pdk_root and 'PDK_ROOT' not in newenv: - newenv['PDK_ROOT'] = pdk_root - if pdk and 'PDK' not in newenv: - newenv['PDK'] = pdk - - info('Extracting netlist with parasitics from layout…') - - magicargs = ['magic', '-dnull', '-noconsole', '-rcfile', rcfile] - dbg('Executing: ' + ' '.join(magicargs)) - - mproc = subprocess.Popen( - magicargs, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=root_path, - env=newenv, - universal_newlines=True, - ) - if magicfilename and os.path.isfile(magicfilename): - mproc.stdin.write('load ' + magicfilename + '\n') - else: - mproc.stdin.write('gds read ' + gdsfilename + '\n') - mproc.stdin.write('load ' + dname + '\n') - # Use readspice to get the port order - mproc.stdin.write('readspice ' + schem_netlist + '\n') - # necessary after readspice - mproc.stdin.write('load ' + dname + '\n') - mproc.stdin.write('select top cell\n') - mproc.stdin.write('expand\n') - mproc.stdin.write('flatten ' + dname + '_flat\n') - mproc.stdin.write('load ' + dname + '_flat\n') - mproc.stdin.write('select top cell\n') - mproc.stdin.write('cellname delete ' + dname + '\n') - mproc.stdin.write('cellname rename ' + dname + '_flat ' + dname + '\n') - mproc.stdin.write('extract path cace_extfiles\n') - mproc.stdin.write('extract all\n') - mproc.stdin.write('ext2sim labels on\n') - mproc.stdin.write('ext2sim -p cace_extfiles\n') - mproc.stdin.write('extresist tolerance 10\n') - mproc.stdin.write('extresist\n') - mproc.stdin.write('ext2spice lvs\n') - mproc.stdin.write('ext2spice cthresh 0.01\n') - mproc.stdin.write('ext2spice extresist on\n') - mproc.stdin.write( - 'ext2spice -p cace_extfiles -o ' + rcx_netlist + '\n' - ) - mproc.stdin.write('quit -noprompt\n') - - magout = mproc.communicate()[0] - printwarn(magout) - if mproc.returncode != 0: - err( - 'Magic process returned error code ' - + str(mproc.returncode) - + '\n' - ) - - if need_rcx_extract and not os.path.isfile(rcx_netlist): - err('No netlist with parasitics extracted from magic.') - - # Remove the temporary directory of extraction files "cace_extfiles" - try: - shutil.rmtree(os.path.join(root_path, 'cace_extfiles')) - except: - warn('Directory for extraction files was not created.') - - # Remove temporary files - try: - os.remove(os.path.join(root_path, dname + '.sim')) - os.remove(os.path.join(root_path, dname + '.nodes')) - except: - warn('.sim and .nodes files were not created.') - - if (mproc.returncode != 0) or ( - need_rcx_extract and not os.path.isfile(rcx_netlist) - ): - return False - - else: - info('Not extracting netlist with parasitics from layout. Up to date.') - - return rcx_netlist - - -def regenerate_lvs_netlist(datasheet, runtime_options, pex=False): +def regenerate_netlist(datasheet, netlist_source, runtime_options, pex=False): """ Regenerate the layout-extracted netlist if out-of-date or if forced. If argument "pex" is True, then generate parasitic capacitances in the output. """ - debug = runtime_options['debug'] force_regenerate = runtime_options['force'] dname = datasheet['name'] netlistname = dname + '.spice' - magicname = dname + '.mag' - gdsname = dname + '.gds' - paths = datasheet['paths'] # Check the "paths" dictionary for paths to various files @@ -468,21 +287,10 @@ def regenerate_lvs_netlist(datasheet, runtime_options, pex=False): else: root_path = '.' - # Magic layout - if 'magic' in paths: - magicpath = paths['magic'] - magicfilename = os.path.join(magicpath, magicname) - else: - magicpath = None - magicfilename = None - - # GDS layout - if 'layout' in paths: - gdspath = paths['layout'] - gdsfilename = os.path.join(gdspath, gdsname) - else: - gdspath = None - gdsfilename = None + # Get the path to the layout, prefer magic if given in datasheet + (layout_filepath, is_magic) = get_layout_path( + dname, paths, check_magic='magic' in paths + ) # Schematic-captured netlist if 'netlist' in paths: @@ -492,47 +300,27 @@ def regenerate_lvs_netlist(datasheet, runtime_options, pex=False): schem_netlist_path = None schem_netlist = None - if pex == True: - nettype = 'pex' - else: - nettype = 'layout' - - # Layout-extracted netlist for LVS - if 'netlist' in paths: - lvs_netlist_path = os.path.join(paths['netlist'], nettype) - lvs_netlist = os.path.join(lvs_netlist_path, netlistname) - else: - lvs_netlist_path = None - lvs_netlist = None - - need_extraction = True + # Path to netlist spice file + netlist_path = os.path.join(paths['netlist'], netlist_source) + netlist_filepath = os.path.join(netlist_path, netlistname) if force_regenerate: - need_lvs_extract = True + dbg(f'Forcing regeneration of {netlist_source} netlist.') + need_extract = True else: - dbg('Checking for out-of-date ' + nettype + ' netlists.') - valid_layoutpath = magicfilename if magicpath else gdsfilename - need_lvs_extract = check_layout_out_of_date( - lvs_netlist, valid_layoutpath, debug + dbg(f'Checking for out of date {netlist_source} netlist.') + need_extract = check_layout_out_of_date( + netlist_filepath, layout_filepath, False ) - if need_lvs_extract: - dbg('Forcing regeneration of layout-extracted netlist.') - - # Layout LVS netlist needs regenerating. Check for magic layout. - if (not magicfilename or not os.path.isfile(magicfilename)) and ( - not gdsfilename or not os.path.isfile(gdsfilename) - ): - err(f'No netlist or layout for project {dname}. ') - if magicfilename: - err(f'(layout master file {magicfilename} not found.)') - else: - err(f'(layout master file {gdsfilename} not found.)') + if need_extract: + if layout_filepath == None: + err(f'Error: No layout for project {dname} found.') return False - # Check for LVS netlist directory - if not os.path.exists(lvs_netlist_path): - os.makedirs(lvs_netlist_path) + # Check for netlist directory + if not os.path.exists(netlist_path): + os.makedirs(netlist_path) if 'PDK_ROOT' in datasheet: pdk_root = datasheet['PDK_ROOT'] @@ -554,58 +342,62 @@ def regenerate_lvs_netlist(datasheet, runtime_options, pex=False): if pdk and 'PDK' not in newenv: newenv['PDK'] = pdk - info('Extracting LVS netlist from layout…') + info(f'Extracting {netlist_source} netlist from layout…') # Assemble stdin for magic magic_input = '' - if magicfilename and os.path.isfile(magicfilename): - magic_input += f'load {magicfilename}\n' + + if is_magic: + magic_input += f'load {layout_filepath}\n' else: - magic_input += f'gds read {gdsfilename}\n' + magic_input += f'gds read {layout_filepath}\n' magic_input += f'load {dname}\n' # Use readspice to get the port order magic_input += f'readspice {schem_netlist}\n' # necessary after readspice magic_input += f'load {dname}\n' - # magic_input += 'select top cell\n' - magic_input += f'select {dname}\n' # TODO? - magic_input += 'expand\n' - magic_input += 'extract path cace_extfiles\n' - magic_input += 'extract all\n' - magic_input += 'ext2spice lvs\n' - if pex == True: + if netlist_source == 'layout' or netlist_source == 'pex': + magic_input += f'select {dname}\n' + magic_input += 'expand\n' + magic_input += 'extract path cace_extfiles\n' + magic_input += 'extract all\n' + magic_input += 'ext2spice lvs\n' + if netlist_source == 'pex': + magic_input += 'ext2spice cthresh 0.01\n' + magic_input += ( + f'ext2spice -p cace_extfiles -o {netlist_filepath}\n' + ) + + if netlist_source == 'rcx': + magic_input += f'select {dname}\n' + magic_input += 'expand\n' + magic_input += f'flatten {dname + "_flat"}\n' + magic_input += f'load {dname + "_flat"}\n' + magic_input += 'select top cell\n' + magic_input += f'cellname delete {dname}\n' + magic_input += f'cellname rename {dname + "_flat"} {dname}\n' + magic_input += 'extract path cace_extfiles\n' + magic_input += 'extract all\n' + magic_input += 'ext2sim labels on\n' + magic_input += 'ext2sim -p cace_extfiles\n' + magic_input += 'extresist tolerance 10\n' + magic_input += 'extresist\n' + magic_input += 'ext2spice lvs\n' magic_input += 'ext2spice cthresh 0.01\n' - magic_input += f'ext2spice -p cace_extfiles -o {lvs_netlist}\n' + magic_input += 'ext2spice extresist on\n' + magic_input += ( + f'ext2spice -p cace_extfiles -o {netlist_filepath}\n' + ) + magic_input += 'quit -noprompt\n' magicargs = ['magic', '-dnull', '-noconsole', '-rcfile', rcfile] - dbg('Executing: ' + ' '.join(magicargs)) - dbg(f'magic stdin:\n{magic_input}') - mproc = subprocess.Popen( - magicargs, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=root_path, - env=newenv, - text=True, + returncode = run_subprocess( + 'magic', magicargs, input=magic_input, write_file=False ) - - mproc.stdin.write(magic_input) - - magout, magerr = mproc.communicate() - - dbg(f'magic stdout:\n{magout}') - dbg(f'magic stderr:\n{magerr}') - - printwarn(magout) - if mproc.returncode != 0: - err(f'Magic process returned error code {mproc.returncode}\n') - - if need_lvs_extract and not os.path.isfile(lvs_netlist): - err('No LVS netlist extracted from magic.') + # printwarn(magout) TODO check if still useful # Remove the extraction files temporary directory "cace_extfiles" try: @@ -613,15 +405,22 @@ def regenerate_lvs_netlist(datasheet, runtime_options, pex=False): except: warn('Directory for extraction files was not created.') - if (mproc.returncode != 0) or ( - need_lvs_extract and not os.path.isfile(lvs_netlist) + # Remove temporary files + try: + os.remove(os.path.join(root_path, dname + '.sim')) + os.remove(os.path.join(root_path, dname + '.nodes')) + except: + dbg('.sim and .nodes files were not created.') + + if (returncode != 0) or ( + need_extract and not os.path.isfile(netlist_filepath) ): return False else: - info('Not extracting LVS netlist from layout. Up to date.') + info(f'Skipping extraction of {netlist_source} netlist. Up to date.') - return lvs_netlist + return netlist_filepath def check_dependencies(datasheet, debug=False): @@ -1035,23 +834,23 @@ def regenerate_netlists(datasheet, runtime_options): # Layout extracted netlist if source == 'layout': - result = regenerate_lvs_netlist(datasheet, runtime_options) + result = regenerate_netlist(datasheet, 'layout', runtime_options) return result # PEX (parasitic capacitance-only) netlist if source == 'pex': - result = regenerate_lvs_netlist(datasheet, runtime_options, pex=True) + result = regenerate_netlist(datasheet, 'pex', runtime_options) # Also make sure LVS netlist is generated, in case LVS is run - regenerate_lvs_netlist(datasheet) + regenerate_netlist(datasheet, 'layout', runtime_options) return result # RCX (R-C-extraction) netlist if source == 'all' or source == 'rcx' or source == 'best': - result = regenerate_rcx_netlist(datasheet, runtime_options) + result = regenerate_netlist(datasheet, 'rcx', runtime_options) # Also make sure LVS netlist is generated, in case LVS is run - regenerate_lvs_netlist(datasheet, runtime_options) + regenerate_netlist(datasheet, 'layout', runtime_options) return result return result diff --git a/cace/common/common.py b/cace/common/common.py index 5d4091c..572cc6e 100644 --- a/cace/common/common.py +++ b/cace/common/common.py @@ -92,6 +92,64 @@ def get_magic_rcfile(): return rcfile +def get_layout_path(projname, paths, check_magic=False): + + # Prefer magic layout + if check_magic and 'magic' in paths: + layout_path = paths['magic'] + layout_filename = projname + '.mag' + layout_filepath = os.path.join(layout_path, layout_filename) + + dbg(f'Trying to find magic layout {layout_filepath}.') + + # Check if magic layout exists + if os.path.isfile(layout_filepath): + + dbg(f'Found magic layout {layout_filepath}!') + + # Return magic layout + return (layout_filepath, True) + + dbg('No magic layout found.') + + # Else use GDSII + if 'layout' in paths: + layout_path = paths['layout'] + layout_filename = projname + '.gds' + layout_filepath = os.path.join(layout_path, layout_filename) + + dbg(f'Trying to find GDS layout {layout_filepath}.') + + # Check if GDS layout exists + if os.path.exists(layout_filepath): + + dbg(f'Found GDS layout {layout_filepath}!') + + # Return GDS layout + return (layout_filepath, False) + + dbg('No GDS layout found.') + dbg('Trying to find compressed GDS layout.') + + layout_path = paths['layout'] + layout_filename = projname + '.gds.gz' + layout_filepath = os.path.join(layout_path, layout_filename) + + # Check if compressed GDS layout exists + if os.path.exists(layout_filepath): + + dbg(f'Found compressed GDS layout {layout_filepath}!') + + # Return compressed GDS layout + return (layout_filepath, False) + + dbg('No compressed GDS layout found.') + + err('Neither magic nor (compressed) GDS layout found.') + + return (None, None, None) + + def get_klayout_techfile(): """ Get the path and filename of the klayout tech file corresponding diff --git a/cace/parameter/parameter_klayout_drc.py b/cace/parameter/parameter_klayout_drc.py index 743bec0..21d0c89 100755 --- a/cace/parameter/parameter_klayout_drc.py +++ b/cace/parameter/parameter_klayout_drc.py @@ -16,7 +16,7 @@ import re import sys -from ..common.common import run_subprocess, get_pdk_root +from ..common.common import run_subprocess, get_pdk_root, get_layout_path from .parameter import Parameter, ResultType, Argument, Result from .parameter_manager import register_parameter from ..logging import ( @@ -73,29 +73,16 @@ def implementation(self): info('Running KLayout to get layout DRC report.') - layout_filename = None - is_mag = False - if 'layout' in paths: - layout_path = paths['layout'] - layoutname = projname + '.gds' - layout_filename = os.path.join(layout_path, layoutname) - if not os.path.exists(layout_filename): - layoutname_gz = projname + '.gds.gz' - layout_filename_gz = os.path.join( - layout_path, layoutname_gz - ) - if not os.path.exists(layout_filename_gz): - err( - f'Layout {layout_filename} or {layout_filename_gz} does not exist!' - ) - self.result_type = ResultType.ERROR - return - layoutname = layoutname_gz - layout_filename = layout_filename_gz - - layout_path = os.path.split(layout_filename)[0] - layout_locname = os.path.split(layout_filename)[1] - layout_cellname = os.path.splitext(layout_locname)[0] + # Get the path to the layout, only GDS + (layout_filepath, is_magic) = get_layout_path( + projname, self.paths, check_magic=False + ) + + # Check if layout exists + if not os.path.isfile(layout_filepath): + err('No layout found!') + self.result_type = ResultType.ERROR + return drc_script_path = os.path.join( get_pdk_root(), @@ -122,9 +109,9 @@ def implementation(self): '-r', drc_script_path, '-rd', - f'input={os.path.abspath(layout_filename)}', + f'input={os.path.abspath(layout_filepath)}', '-rd', - f'topcell={layout_cellname}', + f'topcell={projname}', '-rd', f'report={report_file_path}', '-rd', diff --git a/cace/parameter/parameter_magic_area.py b/cace/parameter/parameter_magic_area.py index 5c0e12c..36f4912 100755 --- a/cace/parameter/parameter_magic_area.py +++ b/cace/parameter/parameter_magic_area.py @@ -20,7 +20,7 @@ import threading import subprocess -from ..common.common import run_subprocess, get_magic_rcfile +from ..common.common import run_subprocess, get_magic_rcfile, get_layout_path from ..common.ring_buffer import RingBuffer from .parameter import Parameter, ResultType, Argument, Result from .parameter_manager import register_parameter @@ -93,57 +93,26 @@ def implementation(self): rcfile = get_magic_rcfile() - # Prefer magic layout - if 'magic' in paths: - magic_path = paths['magic'] - magicname = projname + '.mag' - layout_filename = os.path.join(magic_path, magicname) - is_mag = True - # Else use GDSII - elif 'layout' in paths: - layout_path = paths['layout'] - layoutname = projname + '.gds' - layout_filename = os.path.join(layout_path, layoutname) - # Search for compressed layout - if not os.path.exists(layout_filename): - layoutname = projname + '.gds.gz' - layout_filename = os.path.join(layout_path, layoutname) - else: - err( - 'Neither "magic" nor "layout" specified in datasheet paths.' - ) - self.result_type = ResultType.ERROR - return + # Get the path to the layout, prefer magic + (layout_filepath, is_magic) = get_layout_path( + projname, self.paths, check_magic=True + ) # Check if layout exists - if not os.path.isfile(layout_filename): + if not os.path.isfile(layout_filepath): err('No layout found!') - err(f'Expected file: {layout_filename}') self.result_type = ResultType.ERROR return # Run magic to get the bounds of the design geometry # Get triplet of area, width, and height - is_mag = ( - True - if os.path.splitext(layout_filename)[1] == '.mag' - else False - ) - layout_path = os.path.split(layout_filename)[0] - layout_locname = os.path.split(layout_filename)[1] - layout_cellname = os.path.splitext(layout_locname)[0] - - if not os.path.exists(layout_filename): - err(f'Layout {layout_filename} does not exist!') - magic_input = '' - magic_input += f'addpath {os.path.abspath(layout_path)}\n' - if is_mag: - magic_input += f'load {layout_cellname}\n' + if is_magic: + magic_input += f'load {layout_filepath}\n' else: - magic_input += f'gds read {layout_locname}\n' + magic_input += f'gds read {layout_filepath}\n' magic_input += 'set toplist [cellname list top]\n' magic_input += 'set numtop [llength $toplist]\n' magic_input += 'if {$numtop > 1} {\n' diff --git a/cace/parameter/parameter_magic_drc.py b/cace/parameter/parameter_magic_drc.py index f8fd543..664ffbd 100755 --- a/cace/parameter/parameter_magic_drc.py +++ b/cace/parameter/parameter_magic_drc.py @@ -16,7 +16,7 @@ import re import sys -from ..common.common import run_subprocess, get_magic_rcfile +from ..common.common import run_subprocess, get_magic_rcfile, get_layout_path from .parameter import Parameter, ResultType, Argument, Result from .parameter_manager import register_parameter from ..logging import ( @@ -78,56 +78,30 @@ def implementation(self): info('Running magic to get layout DRC report.') - # Find the layout directory and check if there is a layout - # for the cell there. - - layout_filename = None - is_mag = False - - # Prefer magic layout - if 'magic' in paths: - magic_path = paths['magic'] - magicname = projname + '.mag' - layout_filename = os.path.join(magic_path, magicname) - is_mag = True - # Else use GDSII - elif 'layout' in paths: - layout_path = paths['layout'] - layoutname = projname + '.gds' - layout_filename = os.path.join(layout_path, layoutname) - # Search for compressed layout - if not os.path.exists(layout_filename): - layoutname = projname + '.gds.gz' - layout_filename = os.path.join(layout_path, layoutname) - else: - err( - 'Neither "magic" nor "layout" specified in datasheet paths.' - ) - self.result_type = ResultType.ERROR - return + rcfile = get_magic_rcfile() + + # Get the path to the layout, prefer magic + (layout_filepath, is_magic) = get_layout_path( + projname, self.paths, check_magic=True + ) # Check if layout exists - if not os.path.isfile(layout_filename): + if not os.path.isfile(layout_filepath): err('No layout found!') - err(f'Expected file: {layout_filename}') self.result_type = ResultType.ERROR return - layout_path = os.path.split(layout_filename)[0] - layout_locname = os.path.split(layout_filename)[1] - layout_cellname = os.path.splitext(layout_locname)[0] - - rcfile = get_magic_rcfile() + # Run magic to get the bounds of the design geometry + # Get triplet of area, width, and height magic_input = '' - magic_input += f'addpath {os.path.abspath(layout_path)}\n' - if is_mag: - magic_input += f'load {layout_cellname}\n' + if is_magic: + magic_input += f'load {layout_filepath}\n' else: if self.get_argument('gds_flatten'): magic_input += 'gds flatglob *\n' - magic_input += f'gds read {layout_locname}\n' + magic_input += f'gds read {layout_filepath}\n' magic_input += 'set toplist [cellname list top]\n' magic_input += 'set numtop [llength $toplist]\n' magic_input += 'if {$numtop > 1} {\n'