From 6d21ff81f652861ce3602d2c5661b62d8cf5c9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ko=CC=88hler?= Date: Tue, 10 Dec 2024 19:58:00 +0100 Subject: [PATCH 1/8] kpex/CLI: use output as default for --out_dir --- klayout_pex/kpex_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klayout_pex/kpex_cli.py b/klayout_pex/kpex_cli.py index 6e099b5c..2100208a 100755 --- a/klayout_pex/kpex_cli.py +++ b/klayout_pex/kpex_cli.py @@ -117,7 +117,7 @@ def parse_args(arg_list: List[str] = None) -> argparse.Namespace: group_pex.add_argument("--tech", "-t", dest="tech_pbjson_path", required=True, help="Technology Protocol Buffer path (*.pb.json)") - group_pex.add_argument("--out_dir", "-o", dest="output_dir_base_path", default=".", + group_pex.add_argument("--out_dir", "-o", dest="output_dir_base_path", default="output", help="Output directory path (default is '%(default)s')") group_pex_input = main_parser.add_argument_group("Parasitic Extraction Input", From a2b44d6342d2fc2e51aa32c2c381fdb2aa616e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ko=CC=88hler?= Date: Tue, 10 Dec 2024 20:35:28 +0100 Subject: [PATCH 2/8] argparse_helpers.py: support enums without DEFAULT --- klayout_pex/util/argparse_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klayout_pex/util/argparse_helpers.py b/klayout_pex/util/argparse_helpers.py index 1e3be300..9535da98 100644 --- a/klayout_pex/util/argparse_helpers.py +++ b/klayout_pex/util/argparse_helpers.py @@ -32,10 +32,10 @@ def render_enum_help(topic: str, enum_cls: Type[Enum], print_default: bool = True) -> str: if not hasattr(enum_cls, 'DEFAULT'): - raise ValueError("Enum must declare case 'DEFAULT'") + print_default = False enum_help = f"{topic} ∈ {set([name.lower() for name, member in enum_cls.__members__.items()])}" if print_default: - enum_help += f".\nDefaults to '{enum_cls.DEFAULT.name.lower()}'" + enum_help += f".\nDefaults to '{getattr(enum_cls, 'DEFAULT').name.lower()}'" return enum_help From f7888a90ce242a3d4d1db920a3d7635a30584870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ko=CC=88hler?= Date: Tue, 10 Dec 2024 20:36:00 +0100 Subject: [PATCH 3/8] Add pdk_config.py --- klayout_pex/pdk_config.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 klayout_pex/pdk_config.py diff --git a/klayout_pex/pdk_config.py b/klayout_pex/pdk_config.py new file mode 100644 index 00000000..bca1035a --- /dev/null +++ b/klayout_pex/pdk_config.py @@ -0,0 +1,32 @@ +# +# -------------------------------------------------------------------------------- +# SPDX-FileCopyrightText: 2024 Martin Jan Köhler and Harald Pretl +# Johannes Kepler University, Institute for Integrated Circuits. +# +# This file is part of KPEX +# (see https://github.com/martinjankoehler/klayout-pex). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# SPDX-License-Identifier: GPL-3.0-or-later +# -------------------------------------------------------------------------------- +# + +from dataclasses import dataclass + + +@dataclass +class PDKConfig: + name: str + pex_lvs_script_path: str + tech_pb_json_path: str From 6a59f22bbe7452c72dc52178bf500ac471da574f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ko=CC=88hler?= Date: Tue, 10 Dec 2024 20:37:09 +0100 Subject: [PATCH 4/8] kpex CLI: instead of --tech and --lvs_script, add a --pdk argument for the built-in supported PDKs (ihp_sg13g2, sky130A) --- klayout_pex/kpex_cli.py | 45 ++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/klayout_pex/kpex_cli.py b/klayout_pex/kpex_cli.py index 2100208a..a006f36c 100755 --- a/klayout_pex/kpex_cli.py +++ b/klayout_pex/kpex_cli.py @@ -26,9 +26,11 @@ import argparse from datetime import datetime from enum import StrEnum +from functools import cached_property import logging import os import os.path + import rich.console import rich.markdown import rich.text @@ -65,6 +67,7 @@ rule ) from .magic.magic_runner import MagicPEXMode, run_magic, prepare_magic_script +from .pdk_config import PDKConfig from .rcx25.extractor import RCExtractor, ExtractionResults from .tech_info import TechInfo from .util.multiple_choice import MultipleChoicePattern @@ -82,6 +85,35 @@ class InputMode(StrEnum): GDS = "gds" +# TODO: this should be externally configurable +class PDK(StrEnum): + IHP_SG13G2 = 'ihp_sg13g2' + SKY130A = 'sky130A' + + @cached_property + def config(self) -> PDKConfig: + # NOTE: installation paths of resources in the distribution wheel differes from source repo + base_dir = os.path.dirname(os.path.realpath(__file__)) + tech_pb_json_dir = base_dir + if os.path.isdir(os.path.join(base_dir, '..', '.git')): # in source repo + base_dir = os.path.dirname(base_dir) + tech_pb_json_dir = os.path.join(base_dir, 'build') + + match self: + case PDK.IHP_SG13G2: + return PDKConfig( + name=self, + pex_lvs_script_path=os.path.join(base_dir, 'pdk', self, 'libs.tech', 'kpex', 'sg130g2.lvs'), + tech_pb_json_path=os.path.join(tech_pb_json_dir, f"{self}_tech.pb.json") + ) + case PDK.SKY130A: + return PDKConfig( + name=self, + pex_lvs_script_path=os.path.join(base_dir, 'pdk', self, 'libs.tech', 'kpex', 'sky130.lvs'), + tech_pb_json_path=os.path.join(tech_pb_json_dir, f"{self}_tech.pb.json") + ) + + class KpexCLI: @staticmethod def parse_args(arg_list: List[str] = None) -> argparse.Namespace: @@ -114,8 +146,8 @@ def parse_args(arg_list: List[str] = None) -> argparse.Namespace: help="Path to klayout executable (default is '%(default)s')") group_pex = main_parser.add_argument_group("Parasitic Extraction Setup") - group_pex.add_argument("--tech", "-t", dest="tech_pbjson_path", required=True, - help="Technology Protocol Buffer path (*.pb.json)") + group_pex.add_argument("--pdk", dest="pdk", required=True, type=PDK, + help=render_enum_help(topic='pdk', enum_cls=PDK)) group_pex.add_argument("--out_dir", "-o", dest="output_dir_base_path", default="output", help="Output directory path (default is '%(default)s')") @@ -128,12 +160,7 @@ def parse_args(arg_list: List[str] = None) -> argparse.Namespace: group_pex_input.add_argument("--lvsdb", "-l", dest="lvsdb_path", help="KLayout LVSDB path (bypass LVS)") group_pex_input.add_argument("--cell", "-c", dest="cell_name", default=None, help="Cell (default is the top cell)") - default_lvs_script_path = os.path.realpath(os.path.join(__file__, '..', '..', 'pdk', - 'sky130A', 'libs.tech', 'kpex', 'sky130.lvs')) - group_pex_input.add_argument("--lvs_script", dest="lvs_script_path", - default=default_lvs_script_path, - help=f"Path to KLayout LVS script (default is %(default)s)") group_pex_input.add_argument("--cache-lvs", dest="cache_lvs", type=true_or_false, default=True, help="Used cached LVSDB (for given input GDS) (default is %(default)s)") @@ -231,6 +258,10 @@ def parse_args(arg_list: List[str] = None) -> argparse.Namespace: def validate_args(args: argparse.Namespace): found_errors = False + pdk_config: PDKConfig = args.pdk.config + args.tech_pbjson_path = pdk_config.tech_pb_json_path + args.lvs_script_path = pdk_config.pex_lvs_script_path + if not os.path.isfile(args.klayout_exe_path): path = shutil.which(args.klayout_exe_path) if not path: From b60a89dfb169dec1614ced6bd852e62ecfee77e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ko=CC=88hler?= Date: Tue, 10 Dec 2024 21:20:33 +0100 Subject: [PATCH 5/8] Fixed imports of klayout_pex_protobuf --- klayout_pex/fastercap/fastercap_input_builder.py | 2 +- klayout_pex/klayout/lvsdb_extractor.py | 2 +- klayout_pex/rcx25/extraction_results.py | 2 +- klayout_pex/rcx25/extractor.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/klayout_pex/fastercap/fastercap_input_builder.py b/klayout_pex/fastercap/fastercap_input_builder.py index 70cf51ef..e69e1aef 100644 --- a/klayout_pex/fastercap/fastercap_input_builder.py +++ b/klayout_pex/fastercap/fastercap_input_builder.py @@ -46,7 +46,7 @@ ) from ..tech_info import TechInfo -import klayout_pex_protobuf.process_stack_pb2 +import klayout_pex_protobuf.process_stack_pb2 as process_stack_pb2 class FasterCapInputBuilder: diff --git a/klayout_pex/klayout/lvsdb_extractor.py b/klayout_pex/klayout/lvsdb_extractor.py index e9b55182..063fac4b 100755 --- a/klayout_pex/klayout/lvsdb_extractor.py +++ b/klayout_pex/klayout/lvsdb_extractor.py @@ -30,7 +30,7 @@ import klayout.db as kdb -import klayout_pex_protobuf.tech_pb2 +import klayout_pex_protobuf.tech_pb2 as tech_pb2 from ..log import ( console, debug, diff --git a/klayout_pex/rcx25/extraction_results.py b/klayout_pex/rcx25/extraction_results.py index f368b8cc..4837257e 100644 --- a/klayout_pex/rcx25/extraction_results.py +++ b/klayout_pex/rcx25/extraction_results.py @@ -26,7 +26,7 @@ from dataclasses import dataclass, field from typing import * -import klayout_pex_protobuf.process_parasitics_pb2 +import klayout_pex_protobuf.process_parasitics_pb2 as process_parasitics_pb2 NetName = str diff --git a/klayout_pex/rcx25/extractor.py b/klayout_pex/rcx25/extractor.py index 193c5499..34f737f4 100644 --- a/klayout_pex/rcx25/extractor.py +++ b/klayout_pex/rcx25/extractor.py @@ -40,7 +40,7 @@ ) from ..tech_info import TechInfo from .extraction_results import * -import klayout_pex_protobuf.process_stack_pb2 +import klayout_pex_protobuf.process_stack_pb2 as process_stack_pb2 EdgeInterval = Tuple[float, float] From 5b3122d4b8b0202c2debd1dff71f7d30cdedcb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ko=CC=88hler?= Date: Tue, 10 Dec 2024 21:20:44 +0100 Subject: [PATCH 6/8] Removed obsolete import --- klayout_pex/fastercap/fastercap_input_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klayout_pex/fastercap/fastercap_input_builder.py b/klayout_pex/fastercap/fastercap_input_builder.py index e69e1aef..09c847a7 100644 --- a/klayout_pex/fastercap/fastercap_input_builder.py +++ b/klayout_pex/fastercap/fastercap_input_builder.py @@ -35,7 +35,7 @@ import klayout.db as kdb -from ..klayout.lvsdb_extractor import KLayoutExtractionContext, KLayoutExtractedLayerInfo, GDSPair +from ..klayout.lvsdb_extractor import KLayoutExtractionContext, GDSPair from .fastercap_model_generator import FasterCapModelBuilder, FasterCapModelGenerator from ..log import ( console, From e5947fcc973dfc230a0215c91414e97180fb9a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ko=CC=88hler?= Date: Tue, 10 Dec 2024 21:21:49 +0100 Subject: [PATCH 7/8] kpex CLI: Improved log output --- klayout_pex/fastcap/fastcap_runner.py | 7 +-- .../fastercap/fastercap_model_generator.py | 9 ++-- klayout_pex/fastercap/fastercap_runner.py | 7 +-- klayout_pex/klayout/lvsdb_extractor.py | 5 -- klayout_pex/kpex_cli.py | 46 ++++++++++++++++--- klayout_pex/magic/magic_runner.py | 7 ++- 6 files changed, 57 insertions(+), 24 deletions(-) diff --git a/klayout_pex/fastcap/fastcap_runner.py b/klayout_pex/fastcap/fastcap_runner.py index 4c5c2b0f..fc6cc009 100644 --- a/klayout_pex/fastcap/fastcap_runner.py +++ b/klayout_pex/fastcap/fastcap_runner.py @@ -71,9 +71,10 @@ def run_fastcap(exe_path: str, f"-l{lst_file_path}", ] - info(f"Calling {' '.join(args)}, output file: {log_path}") + info(f"Calling FastCap2") + subproc(f"{' '.join(args)}, output file: {log_path}") - rule() + rule('FastCap Output') start = time.time() proc = subprocess.Popen(args, @@ -98,7 +99,7 @@ def run_fastcap(exe_path: str, if proc.returncode == 0: info(f"FastCap2 succeeded after {'%.4g' % duration}s") else: - raise Exception(f"FastCap2 failed with status code {proc.returncode} after {'%.4g' % duration}s", + raise Exception(f"FastCap2 failed with status code {proc.returncode} after {'%.4g' % duration}s, " f"see log file: {log_path}") diff --git a/klayout_pex/fastercap/fastercap_model_generator.py b/klayout_pex/fastercap/fastercap_model_generator.py index 1a4da99e..4ece5288 100644 --- a/klayout_pex/fastercap/fastercap_model_generator.py +++ b/klayout_pex/fastercap/fastercap_model_generator.py @@ -75,7 +75,8 @@ debug, info, warning, - error + error, + subproc ) @@ -810,7 +811,7 @@ def write_fastcap(self, output_dir_path: str, prefix: str) -> str: collation_operator = '' if idx == last_cond_index else ' +' lst_file.append(f"C {fn} {'%.12g' % k_outside} 0 0 0{collation_operator}") - info(f"Writing FasterCap list file: {lst_fn}") + subproc(lst_fn) with open(lst_fn, "w") as f: f.write('\n'.join(lst_file)) f.write('\n') @@ -823,7 +824,7 @@ def _write_fastercap_geo(output_path: str, cond_number: int, cond_name: Optional[str], rename_conductor: bool): - info(f"Writing FasterCap geo file: {output_path}") + subproc(output_path) with open(output_path, "w") as f: f.write(f"0 GEO File\n") for t in data: @@ -973,7 +974,7 @@ def _write_as_stl(file_name: str, if len(tris) == 0: return - info(f"Writing STL file {file_name}") + subproc(file_name) with open(file_name, "w") as f: f.write("solid stl\n") for t in tris: diff --git a/klayout_pex/fastercap/fastercap_runner.py b/klayout_pex/fastercap/fastercap_runner.py index 1ff3d0e2..cff75adf 100644 --- a/klayout_pex/fastercap/fastercap_runner.py +++ b/klayout_pex/fastercap/fastercap_runner.py @@ -70,9 +70,10 @@ def run_fastercap(exe_path: str, args += [ lst_file_path ] - info(f"Calling {' '.join(args)}, output file: {log_path}") + info(f"Calling FasterCap") + subproc(f"{' '.join(args)}, output file: {log_path}") - rule() + rule('FasterCap Output') start = time.time() proc = subprocess.Popen(args, @@ -97,7 +98,7 @@ def run_fastercap(exe_path: str, if proc.returncode == 0: info(f"FasterCap succeeded after {'%.4g' % duration}s") else: - raise Exception(f"FasterCap failed with status code {proc.returncode} after {'%.4g' % duration}s", + raise Exception(f"FasterCap failed with status code {proc.returncode} after {'%.4g' % duration}s, " f"see log file: {log_path}") diff --git a/klayout_pex/klayout/lvsdb_extractor.py b/klayout_pex/klayout/lvsdb_extractor.py index 063fac4b..05c965b8 100755 --- a/klayout_pex/klayout/lvsdb_extractor.py +++ b/klayout_pex/klayout/lvsdb_extractor.py @@ -119,11 +119,6 @@ def prepare_extraction(cls, tech=tech, blackbox_devices=blackbox_devices) - rule('Non-empty layers in LVS database:') - for gds_pair, layer_info in extracted_layers.items(): - names = [l.lvs_layer_name for l in layer_info.source_layers] - info(f"{gds_pair} -> ({' '.join(names)})") - return KLayoutExtractionContext( lvsdb=lvsdb, dbu=dbu, diff --git a/klayout_pex/kpex_cli.py b/klayout_pex/kpex_cli.py index a006f36c..f9cfa91f 100755 --- a/klayout_pex/kpex_cli.py +++ b/klayout_pex/kpex_cli.py @@ -80,6 +80,10 @@ PROGRAM_NAME = "kpex" +class ArgumentValidationError(Exception): + pass + + class InputMode(StrEnum): LVSDB = "lvsdb" GDS = "gds" @@ -272,6 +276,8 @@ def validate_args(args: argparse.Namespace): error(f"Can't read technology file at path {args.tech_pbjson_path}") found_errors = True + rule('Input Layout') + # input mode: LVS or existing LVSDB? if args.gds_path: info(f"GDS input file passed, running in LVS mode") @@ -381,8 +387,23 @@ def input_file_stem(path: str): error("Failed to parse --diel arg", e) found_errors = True + # at least one engine must be activated + + print("m#äh") + if not (args.run_magic or args.run_fastcap or args.run_fastercap or args.run_2_5D): + error("No PEX engines activated") + engine_help = """ +| Argument | Description | +| -------------- | --------------------------------------- | +| --fastercap y | Run kpex/FasterCap engine | +| --2.5D y | Run kpex/2.5D engine | +| --magic y | Run MAGIC engine | +""" + subproc(f"\nPlease activate one or more engines using the arguments:\n{engine_help}") + found_errors = True + if found_errors: - raise Exception("Argument validation failed") + raise ArgumentValidationError("Argument validation failed") def build_fastercap_input(self, args: argparse.Namespace, @@ -396,17 +417,19 @@ def build_fastercap_input(self, delaunay_b=args.delaunay_b) gen: FasterCapModelGenerator = fastercap_input_builder.build() - rule() + rule('FasterCap Input File Generation') faster_cap_input_dir_path = os.path.join(args.output_dir_path, 'FasterCap_Input_Files') os.makedirs(faster_cap_input_dir_path, exist_ok=True) lst_file = gen.write_fastcap(output_dir_path=faster_cap_input_dir_path, prefix='FasterCap_Input_') + rule('STL File Generation') geometry_dir_path = os.path.join(args.output_dir_path, 'Geometries') os.makedirs(geometry_dir_path, exist_ok=True) gen.dump_stl(output_dir_path=geometry_dir_path, prefix='') if args.geometry_check: + rule('Geometry Validation') gen.check() return lst_file @@ -416,6 +439,7 @@ def run_fastercap_extraction(self, args: argparse.Namespace, pex_context: KLayoutExtractionContext, lst_file: str): + rule('FasterCap Execution') info(f"Configure number of OpenMP threads (environmental variable OMP_NUM_THREADS) as {args.num_threads}") os.environ['OMP_NUM_THREADS'] = f"{args.num_threads}" @@ -520,6 +544,7 @@ def run_fastcap_extraction(self, args: argparse.Namespace, pex_context: KLayoutExtractionContext, lst_file: str): + rule('FastCap2 Execution') exe_path = "fastcap" log_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Output.txt") raw_csv_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_FastCap2_Result_Matrix_Raw.csv") @@ -634,7 +659,7 @@ def reregister_log_file_handler(handler: logging.Handler, file_handler_formatted = register_log_file_handler(cli_log_path_formatted, formatter) try: self.validate_args(args) - except Exception: + except ArgumentValidationError: if hasattr(args, 'output_dir_path'): reregister_log_file_handler(file_handler_plain, cli_log_path_plain, None) reregister_log_file_handler(file_handler_formatted, cli_log_path_formatted, formatter) @@ -663,7 +688,8 @@ def create_lvsdb(self, args: argparse.Namespace) -> kdb.LayoutVsSchematic: if os.path.exists(lvsdb_path) and args.cache_lvs: if self.modification_date(lvsdb_path) > self.modification_date(args.gds_path): - warning(f"Reusing cached LVSDB at {lvsdb_path}") + warning(f"Reusing cached LVSDB") + subproc(lvsdb_path) lvs_needed = False if lvs_needed: @@ -682,8 +708,8 @@ def main(self, argv: List[str]): '--version' not in argv and \ '-h' not in argv and \ '--help' not in argv: - info("Called with arguments:") - info(' '.join(map(shlex.quote, sys.argv))) + rule('Command line arguments') + subproc(' '.join(map(shlex.quote, sys.argv))) args = self.parse_args(argv[1:]) @@ -694,19 +720,25 @@ def main(self, argv: List[str]): dielectric_filter=args.dielectric_filter) if args.run_magic: - rule("MAGIC") + rule('MAGIC') self.run_magic_extraction(args) # no need to run LVS etc if only running magic engine if not (args.run_fastcap or args.run_fastercap or args.run_2_5D): return + rule('Prepare LVSDB') lvsdb = self.create_lvsdb(args) pex_context = KLayoutExtractionContext.prepare_extraction(top_cell=args.effective_cell_name, lvsdb=lvsdb, tech=tech_info, blackbox_devices=args.blackbox_devices) + rule('Non-empty layers in LVS database') + for gds_pair, layer_info in pex_context.extracted_layers.items(): + names = [l.lvs_layer_name for l in layer_info.source_layers] + info(f"{gds_pair} -> ({' '.join(names)})") + gds_path = os.path.join(args.output_dir_path, f"{args.effective_cell_name}_l2n_extracted.gds.gz") pex_context.target_layout.write(gds_path) diff --git a/klayout_pex/magic/magic_runner.py b/klayout_pex/magic/magic_runner.py index 708301c9..3bc4cf7b 100644 --- a/klayout_pex/magic/magic_runner.py +++ b/klayout_pex/magic/magic_runner.py @@ -120,7 +120,10 @@ def run_magic(exe_path: str, script_path, # TCL script ] - info(f"Calling {' '.join(args)}, output file: {log_path}") + info('Calling MAGIC') + subproc(f"{' '.join(args)}, output file: {log_path}") + + rule('MAGIC Output') start = time.time() @@ -146,6 +149,6 @@ def run_magic(exe_path: str, if proc.returncode == 0: info(f"MAGIC succeeded after {'%.4g' % duration}s") else: - raise Exception(f"MAGIC failed with status code {proc.returncode} after {'%.4g' % duration}s", + raise Exception(f"MAGIC failed with status code {proc.returncode} after {'%.4g' % duration}s, " f"see log file: {log_path}") From 5008ad5ef964c31a0b185364957c802a97e8e16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ko=CC=88hler?= Date: Tue, 10 Dec 2024 21:22:09 +0100 Subject: [PATCH 8/8] .gitignore: exclude output --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a662f0b6..45506e96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Project Artifacts testdata/designs/**/*.cir -output_* +output* # Mac .DS_Store