gdsfill 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gdsfill-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: gdsfill
3
+ Version: 0.1.0
4
+ Summary: A Python project using pyyaml, packaging, and gdstk.
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: homepage, https://example.com
8
+ Project-URL: repository, https://github.com/yourusername/my-python-project
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pyyaml
12
+ Requires-Dist: packaging
13
+ Requires-Dist: gdstk
@@ -0,0 +1,98 @@
1
+ gdsfill
2
+ =======
3
+
4
+ **gdsfill** is an open-source tool for inserting dummy metal fill into semiconductor layouts.
5
+ It helps designers meet density requirements and prepare GDSII layouts for manufacturing by analyzing, erasing, and generating dummy fill patterns across multiple layers.
6
+ The tool is designed to integrate easily into existing design flows and ensures reproducible, automated preparation of layouts before tape-out.
7
+
8
+ This project is still under development. Please report any issues you encounter and always verify your layout before tape-out deadlines to prevent submission failures.
9
+
10
+ Installation
11
+ ############
12
+
13
+ **gdsfill** can be installed as a Python package. We recommend using a virtual environment to keep dependencies isolated.
14
+
15
+ .. code-block:: text
16
+
17
+ $ python3 -m venv venv
18
+ $ source venv/bin/activate
19
+ (venv) $ pip install --updage pip
20
+ (venv) $ pip install gdsfill
21
+
22
+ Density
23
+ #######
24
+
25
+ This command calculates the utilization per layer and prints the values.
26
+ It is useful to check layer density before and after running the fill process:
27
+
28
+ .. code-block:: text
29
+
30
+ gdsfill density <my-layout.gds>
31
+
32
+ Erase
33
+ #####
34
+
35
+ If a layout already contains dummy fill, or if previous fills should be removed, this command erases all dummy metal fill from a layout:
36
+
37
+ .. code-block:: text
38
+
39
+ gdsfill erase <my-layout.gds>
40
+
41
+ Fill
42
+ ####
43
+
44
+ To insert dummy metal fill into all supported layers of a layout, run:
45
+
46
+ .. code-block:: text
47
+
48
+ gdsfill fill <my-layout.gds>
49
+
50
+ Some algorithms, such as the track filler, require information about the chip core region. You can provide the lower-left and upper-right coordinates as floating-point values:
51
+
52
+ .. code-block:: text
53
+
54
+ gdsfill fill <my-layout.gds> --core-size llx lly urx ury
55
+
56
+ By default, **gdsfill** creates a temporary directory for intermediate data.
57
+ Use ``--keep-data`` to retain all generated files in a directory called ``gdsfill-tmp``:
58
+
59
+ .. code-block:: text
60
+
61
+ gdsfill fill <my-layout.gds> --keep-data
62
+
63
+ If you only want to simulate the process without modifying the layout file, use ``--dry-run``:
64
+
65
+ .. code-block:: text
66
+
67
+ gdsfill fill <my-layout.gds> --dry-run
68
+
69
+
70
+ Custom Configuration
71
+ ####################
72
+
73
+ By default, **gdsfill** inserts dummy metal fill into each layer using predefined parameters.
74
+ To apply different parameters or restrict fill to specific layers, you can create a custom configuration file.
75
+
76
+ The following example config inserts fill only into **TopMetal1** and **TopMetal2**:
77
+
78
+ .. code-block:: yaml
79
+
80
+ PDK: ihp-sg13g2
81
+ layers:
82
+ TopMetal1:
83
+ algorithm: Square
84
+ density: 60
85
+ deviation: 1
86
+ TopMetal2:
87
+ algorithm: Square
88
+ density: 60
89
+ deviation: 1
90
+
91
+ .. note::
92
+ Example config files are available in ``gdsfill/configs``.
93
+
94
+ To use a custom config file, pass it with ``--config-file``:
95
+
96
+ .. code-block:: text
97
+
98
+ gdsfill fill <my-layout.gds> --config-file <my-config-file.yaml>
File without changes
@@ -0,0 +1,211 @@
1
+ """
2
+ Command-line interface for GDSII dummy fill operations.
3
+
4
+ This module provides subcommands to:
5
+ - Insert dummy fill into a layout (`fill`)
6
+ - Erase dummy fill from a layout (`erase`)
7
+ - Calculate per-layer density (`density`)
8
+
9
+ Dummy fill is generated tile-by-tile using processes defined
10
+ in the PDK, and relies on Klayout for GDSII operations.
11
+ """
12
+
13
+ import argparse
14
+ import tempfile
15
+ import importlib
16
+ from pathlib import Path
17
+ from multiprocessing import Process
18
+
19
+ from gdsfill.library.klayout import (
20
+ get_version,
21
+ export_layer,
22
+ print_density,
23
+ erase_fill,
24
+ merge_tile
25
+ )
26
+ from gdsfill.library.common import (
27
+ PdkInformation,
28
+ Tile,
29
+ open_yaml
30
+ )
31
+ from gdsfill.library.fill import fill_layer
32
+
33
+
34
+ # pylint: disable=too-many-locals, too-many-arguments, too-many-positional-arguments
35
+ def _fill_layer(layer, pdk, inputfile, tmpdirname, dry_run, core_size=None):
36
+ """
37
+ Run the fill pipeline for a single layer.
38
+
39
+ Steps:
40
+ 1. Export layer geometry into tiles.
41
+ 2. Modify tiles using the process-specific `prepare_tile`.
42
+ 3. Apply dummy fill to each tile with the selected algorithms.
43
+ 4. Optionally merge tiles back into a single GDS file.
44
+
45
+ Args:
46
+ layer (str): Target layer name.
47
+ pdk (PdkInformation): PDK instance with layer rules.
48
+ inputfile (Path): Path to the input GDS file.
49
+ tmpdirname (Path | str): Temporary directory for intermediate data.
50
+ dry_run (bool): If True, skip merging filled tiles.
51
+
52
+ Returns:
53
+ None
54
+ """
55
+ print(f"--- Layer {layer} ---")
56
+
57
+ output_path = Path(tmpdirname) / layer
58
+ for stage in ('raw', 'modified', 'filled'):
59
+ (output_path / stage).mkdir(parents=True, exist_ok=True)
60
+
61
+ export_layer(pdk, inputfile, output_path, layer, core_size)
62
+ tiles = open_yaml(output_path / "tiles.yaml")
63
+
64
+ prepare_module = importlib.import_module(f'gdsfill.{pdk.get_name()}.prepare')
65
+ procs_modify = []
66
+ for tile in tiles['tiles'].keys():
67
+ raw_tile = output_path / "raw" / f"tile_{tile}.gds"
68
+ proc = Process(target=prepare_module.prepare_tile, args=(pdk, raw_tile, layer))
69
+ procs_modify.append(proc)
70
+ proc.start()
71
+ for proc in procs_modify:
72
+ proc.join()
73
+
74
+ procs_fill = []
75
+ for tile, values in tiles['tiles'].items():
76
+ file = output_path / "modified" / f"tile_{tile}.gds"
77
+ proc = Process(target=fill_layer,
78
+ args=(pdk, file, layer, tiles, Tile(values['x'], values['y'])))
79
+ procs_fill.append(proc)
80
+ proc.start()
81
+ for proc in procs_fill:
82
+ proc.join()
83
+
84
+ if dry_run:
85
+ print("Skip merging filled tiles because --dry-run was passed.")
86
+ else:
87
+ merge_tile(pdk, inputfile, output_path / "filled", output_path / "tiles.yaml")
88
+
89
+
90
+ def func_fill(args, pdk):
91
+ """
92
+ Subcommand: Insert dummy fill into each layer of a GDS file.
93
+
94
+ Args:
95
+ args (Namespace): Parsed CLI arguments.
96
+ pdk (PdkInformation): PDK instance with layer rules.
97
+
98
+ Returns:
99
+ None
100
+ """
101
+ if args.keep_data:
102
+ tmpdirname = Path.cwd() / "gdsfill-tmp"
103
+ print(f"Data are stored in {tmpdirname}")
104
+ for layer, _ in pdk.get_layers():
105
+ _fill_layer(layer, pdk, args.input, tmpdirname, args.dry_run, args.core_size)
106
+ else:
107
+ with tempfile.TemporaryDirectory(prefix='gdsfill-') as tmpdirname:
108
+ for layer, _ in pdk.get_layers():
109
+ _fill_layer(layer, pdk, args.input, tmpdirname, args.dry_run, args.core_size)
110
+
111
+
112
+ def func_density(args, pdk):
113
+ """
114
+ Subcommand: Calculate density for each layer of a GDS file.
115
+
116
+ Args:
117
+ args (Namespace): Parsed CLI arguments.
118
+ pdk (PdkInformation): PDK instance with layer rules.
119
+
120
+ Returns:
121
+ None
122
+ """
123
+ print_density(pdk, args.input)
124
+
125
+
126
+ def func_erase(args, pdk):
127
+ """
128
+ Subcommand: Erase all dummy fill from a GDS file.
129
+
130
+ Args:
131
+ args (Namespace): Parsed CLI arguments.
132
+ pdk (PdkInformation): PDK instance with layer rules.
133
+
134
+ Returns:
135
+ None
136
+ """
137
+ erase_fill(pdk, args.input)
138
+
139
+
140
+ def is_valid_file(value: str):
141
+ """
142
+ Argparse validator: ensure argument points to an existing file.
143
+
144
+ Args:
145
+ value (str): Path to the file.
146
+
147
+ Returns:
148
+ Path: Validated Path object.
149
+
150
+ Raises:
151
+ argparse.ArgumentTypeError: If the path does not exist or is not a file.
152
+ """
153
+ file_ = Path(value)
154
+ if file_.is_file():
155
+ return file_
156
+ raise argparse.ArgumentTypeError(f"File {value} doesn't exist!")
157
+
158
+
159
+ def arguments():
160
+ """
161
+ Define CLI arguments and subcommands.
162
+
163
+ Returns:
164
+ argparse.Namespace: Parsed command-line arguments.
165
+ """
166
+ parser = argparse.ArgumentParser()
167
+ parser.add_argument("--process", default="ihp-sg13g2")
168
+ subparsers = parser.add_subparsers(help='subcommand help')
169
+ fill = subparsers.add_parser('fill', help='Fill chip with dummy metal')
170
+ fill.add_argument("input", type=is_valid_file)
171
+ fill.add_argument('--keep-data', action=argparse.BooleanOptionalAction)
172
+ fill.add_argument('--dry-run', action=argparse.BooleanOptionalAction)
173
+ fill.add_argument('--config-file', type=is_valid_file)
174
+ fill.add_argument('--core-size', type=float, nargs=4, metavar=('llx', 'lly', 'urx', 'ury'),
175
+ help="lower left (x, y) and upper right (x, y) points of the chip core.")
176
+ fill.set_defaults(func=func_fill)
177
+
178
+ erase = subparsers.add_parser('erase', help='Erase dummy metal from chip')
179
+ erase.add_argument("input", type=is_valid_file)
180
+ erase.set_defaults(func=func_erase)
181
+
182
+ density = subparsers.add_parser('density', help='Calculate density for each layer')
183
+ density.add_argument("input", type=is_valid_file)
184
+ density.set_defaults(func=func_density)
185
+
186
+ return parser.parse_args()
187
+
188
+
189
+ def main():
190
+ """
191
+ Entry point for the gdsfill CLI.
192
+
193
+ Parses arguments, checks Klayout version compatibility,
194
+ and dispatches to the selected subcommand.
195
+
196
+ Returns:
197
+ int: Exit code (0 on success, nonzero on error).
198
+ """
199
+ args = arguments()
200
+ pdk = PdkInformation(args.process, args.config_file if 'config_file' in args else None)
201
+ klayout_version = get_version()
202
+ if klayout_version < pdk.get_minimum_klayout_version():
203
+ print(f"Please install Klayout {pdk.get_minimum_klayout_version()}")
204
+ return 1
205
+
206
+ args.func(args, pdk)
207
+ return 0
208
+
209
+
210
+ if __name__ == '__main__':
211
+ main()
File without changes
@@ -0,0 +1,245 @@
1
+ """
2
+ Common utilities for handling PDK configuration and tile metadata.
3
+
4
+ This module provides:
5
+ - `PdkInformation`: an interface for accessing process design kit (PDK)
6
+ configuration, constants, and layer-specific parameters.
7
+ - `Tile`: a dataclass representing the position of a tile in the layout.
8
+ - Utility functions for validating input files and loading YAML
9
+ configuration files.
10
+
11
+ The functions and classes in this module are used across the GDS fill
12
+ workflow for tasks such as density calculation, layer export, and tile
13
+ processing.
14
+ """
15
+
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ import yaml
19
+ from packaging.version import Version
20
+
21
+
22
+ class PdkInformation:
23
+ """
24
+ Accessor for PDK configuration and constants.
25
+
26
+ Loads a process-specific configuration (`configs/`) and
27
+ constants (`constants.yaml`) from the PDK directory and provides
28
+ convenient methods to query layer settings, algorithms, density
29
+ requirements, and rules for dummy fill.
30
+
31
+ Attributes:
32
+ data (dict): Contents of the process config file.
33
+ constants (dict): Contents of the process `constants.yaml`.
34
+ """
35
+
36
+ def __init__(self, process, config_file):
37
+ """
38
+ Initialize a PDK information object.
39
+
40
+ Args:
41
+ process (str): Name of the process directory (e.g., "ihp-sg13g2").
42
+ config_file (Path or str): Optional path to a custom config file.
43
+ If None, defaults to `configs/<process>.yaml`.
44
+ """
45
+ script = Path(__file__).parent.parent.resolve()
46
+ self.data = open_yaml(config_file if config_file else script / f"configs/{process}.yaml")
47
+ self.constants = open_yaml(script / process / "constants.yaml")
48
+
49
+ def get_minimum_klayout_version(self):
50
+ """
51
+ Get the minimum required KLayout version for this PDK.
52
+
53
+ Returns:
54
+ Version: Required version parsed as a `packaging.version.Version`.
55
+ """
56
+ return Version(self.constants['minimum_klayout'])
57
+
58
+ def get_name(self):
59
+ """
60
+ Get the name of the PDK.
61
+
62
+ Returns:
63
+ str: Name of the process design kit.
64
+ """
65
+ return self.data['PDK']
66
+
67
+ def get_layers(self):
68
+ """
69
+ Get all configured layers for this PDK.
70
+
71
+ Returns:
72
+ Iterable[Tuple[str, dict]]: List of (layer_name, properties).
73
+ """
74
+ return self.data['layers'].items()
75
+
76
+ def get_layer(self, layer: str) -> dict:
77
+ """
78
+ Get configuration details for a specific layer.
79
+
80
+ Args:
81
+ layer (str): Layer name.
82
+
83
+ Returns:
84
+ dict: Layer configuration dictionary.
85
+ """
86
+ return self.data['layers'][layer]
87
+
88
+ def get_layer_tile_width(self, layer: str) -> int:
89
+ """
90
+ Get the tile width for a given layer.
91
+
92
+ Args:
93
+ layer (str): Layer name.
94
+
95
+ Returns:
96
+ int: Tile width in database units.
97
+ """
98
+ if 'tile_width' in self.get_layer(layer):
99
+ return int(self.get_layer(layer)['tile_width'])
100
+ return int(self.constants['tile_width'])
101
+
102
+ def get_layer_algorithm(self, layer: str) -> list:
103
+ """
104
+ Get the fill algorithm(s) for a given layer.
105
+
106
+ Args:
107
+ layer (str): Layer name.
108
+
109
+ Returns:
110
+ List[str]: List of algorithm names.
111
+ """
112
+ algo = self.get_layer(layer)['algorithm']
113
+ if isinstance(algo, str):
114
+ return [algo]
115
+ return algo
116
+
117
+ def get_layer_density(self, layer: str) -> float:
118
+ """
119
+ Get the target metal density for a layer.
120
+
121
+ Args:
122
+ layer (str): Layer name.
123
+
124
+ Returns:
125
+ float: Target density percentage.
126
+ """
127
+ return self.get_layer(layer)['density']
128
+
129
+ def get_layer_deviation(self, layer: str) -> float:
130
+ """
131
+ Get the allowed density deviation for a layer.
132
+
133
+ Args:
134
+ layer (str): Layer name.
135
+
136
+ Returns:
137
+ float: Allowed deviation percentage.
138
+ """
139
+ return self.get_layer(layer)['deviation']
140
+
141
+ def get_layer_index(self, layer: str) -> int:
142
+ """
143
+ Get the numeric index for a given layer.
144
+
145
+ Args:
146
+ layer (str): Layer name.
147
+
148
+ Returns:
149
+ int: Layer index.
150
+ """
151
+ return self.constants['layers'][layer]['index']
152
+
153
+ def get_layer_fill_datatype(self, layer: str) -> int:
154
+ """
155
+ Get the GDS datatype used for fill shapes of a layer.
156
+
157
+ Args:
158
+ layer (str): Layer name.
159
+
160
+ Returns:
161
+ int: Fill datatype.
162
+ """
163
+ return self.constants['layers'][layer]['fill']
164
+
165
+ def get_layer_max_depth(self, layer: str) -> int:
166
+ """
167
+ Get the maximum recursion depth for fill placement.
168
+
169
+ Args:
170
+ layer (str): Layer name.
171
+
172
+ Returns:
173
+ int: Maximum recursion depth.
174
+ """
175
+ return self.constants['layers'][layer]['max_depth']
176
+
177
+ def has_fill_algorithm(self, layer: str, algorithm: str) -> bool:
178
+ """
179
+ Check if a layer supports a given fill algorithm.
180
+
181
+ Args:
182
+ layer (str): Layer name.
183
+ algorithm (str): Algorithm identifier.
184
+
185
+ Returns:
186
+ bool: True if supported, False otherwise.
187
+ """
188
+ return self.constants['layers'][layer].get(algorithm) is not None
189
+
190
+ def get_fill_rules(self, layer: str, algorithm: str) -> dict:
191
+ """
192
+ Get the rule set for a given layer and fill algorithm.
193
+
194
+ Args:
195
+ layer (str): Layer name.
196
+ algorithm (str): Algorithm identifier.
197
+
198
+ Returns:
199
+ dict: Dictionary of fill rules (e.g., min/max size, spacing).
200
+ """
201
+ return self.constants['layers'][layer][algorithm]
202
+
203
+
204
+ @dataclass
205
+ class Tile:
206
+ """
207
+ Representation of a tile’s position in the layout.
208
+
209
+ Attributes:
210
+ x (int): X-coordinate of the tile.
211
+ y (int): Y-coordinate of the tile.
212
+ """
213
+
214
+ x: int
215
+ y: int
216
+
217
+
218
+ def inputfile_exists(inputfile: str):
219
+ """
220
+ Check if a given file path exists.
221
+
222
+ Args:
223
+ inputfile (str): Path to the file.
224
+
225
+ Returns:
226
+ bool: True if the file exists, False otherwise.
227
+ """
228
+ file_ = Path(inputfile)
229
+ return file_.exists()
230
+
231
+
232
+ def open_yaml(yamlfile: Path):
233
+ """
234
+ Load the contents of a YAML file.
235
+
236
+ Args:
237
+ yamlfile (Path): Path to the YAML file.
238
+
239
+ Returns:
240
+ dict or bool: Parsed YAML content, or False if the file does not exist.
241
+ """
242
+ if not yamlfile.exists():
243
+ return False
244
+ content = Path(yamlfile).read_text(encoding='utf-8')
245
+ return yaml.safe_load(content)
@@ -0,0 +1,68 @@
1
+ """
2
+ Layer filler driver.
3
+
4
+ Coordinates filler insertion by selecting the appropriate algorithm
5
+ and applying it to a given tile and layer.
6
+ """
7
+ import gdstk
8
+
9
+ from gdsfill.library.filler.helper import calculate_density
10
+ from gdsfill.library.filler.overlap import fill_overlap
11
+ from gdsfill.library.filler.square import fill_square
12
+ from gdsfill.library.filler.track import fill_track
13
+
14
+
15
+ ALGOS = {
16
+ 'Overlap': fill_overlap,
17
+ 'Square': fill_square,
18
+ 'Track': fill_track,
19
+ }
20
+
21
+
22
+ def fill_layer(pdk, inputfile, layer, tiles, tile):
23
+ """
24
+ Fill a layout layer to meet density requirements.
25
+
26
+ Reads a GDS file, checks density, and applies one or more filler
27
+ algorithms (square or track) as defined in the PDK.
28
+
29
+ Args:
30
+ pdk (object): Provides layer rules and supported algorithms.
31
+ inputfile (Path | str): Path to the input GDS file.
32
+ layer (str): Target layer name.
33
+ tiles (dict): Tiling information for the layout.
34
+ tile (object): Current tile instance.
35
+
36
+ Returns:
37
+ bool: True if successful, False if algorithm unsupported.
38
+ """
39
+ library = gdstk.read_gds(inputfile, unit=1e-6)
40
+ annotated_cell = library.top_level()[0]
41
+
42
+ metal_density = calculate_density(annotated_cell)
43
+ desired_density = pdk.get_layer_density(layer)
44
+ fill_algos = pdk.get_layer_algorithm(layer)
45
+
46
+ print(f"Filling Tile {tile.x}x{tile.y}")
47
+ print(f"Metal density: {metal_density} %")
48
+ print(f"Desired density: {desired_density} % with {pdk.get_layer_deviation(layer)} % deviation")
49
+ print(f"Fill algorithm: {', '.join(fill_algos)}")
50
+
51
+ for fill_algo in fill_algos:
52
+ if fill_algo not in ALGOS:
53
+ print(f"Unknown fill algorithm {fill_algo} for layer {layer}")
54
+ return False
55
+
56
+ for fill_algo in fill_algos:
57
+ if not pdk.has_fill_algorithm(layer, fill_algo):
58
+ print(f"Unsupported fill algorithm {fill_algo} for layer {layer}")
59
+ return False
60
+
61
+ fill_lib = gdstk.Library("fill")
62
+ if metal_density < desired_density:
63
+ for fill_algo in fill_algos:
64
+ fill_cell = ALGOS[fill_algo](pdk, layer, tiles, tile, annotated_cell)
65
+ fill_lib.add(fill_cell)
66
+
67
+ fill_lib.write_gds(str(inputfile).replace('modified', 'filled'))
68
+ return True
File without changes