haadic 0.6.0__py3-none-any.whl

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.
Files changed (75) hide show
  1. haadic/__init__.py +21 -0
  2. haadic/_config.py +5 -0
  3. haadic/core/__init__.py +1 -0
  4. haadic/core/flow.py +111 -0
  5. haadic/core/steps/__init__.py +1 -0
  6. haadic/core/steps/extraction.py +53 -0
  7. haadic/core/steps/layout_generation.py +59 -0
  8. haadic/core/steps/post_process.py +58 -0
  9. haadic/core/steps/spice_simulation.py +63 -0
  10. haadic/core/steps/step.py +226 -0
  11. haadic/core/techno.py +230 -0
  12. haadic/core/tools.py +98 -0
  13. haadic/data/mockup_pdk/ekv_mock.json +15 -0
  14. haadic/data/mockup_pdk/mock.json +153 -0
  15. haadic/data/mockup_pdk/mock.map +13 -0
  16. haadic/data/mockup_pdk/mock.proc +45 -0
  17. haadic/data/mockup_pdk/mock.tlef +80 -0
  18. haadic/data/patches/gf180mcu.json +84 -0
  19. haadic/data/patches/sky130.json +182 -0
  20. haadic/data/techno.json +38 -0
  21. haadic/data/template/cookiecutter.json +5 -0
  22. haadic/data/template/{{cookiecutter.project_name}}/bench.cir +9 -0
  23. haadic/data/template/{{cookiecutter.project_name}}/design.py +77 -0
  24. haadic/design/__init__.py +10 -0
  25. haadic/design/components/__init__.py +1 -0
  26. haadic/design/components/ekv.py +304 -0
  27. haadic/design/components/ekv_bench.cir +16 -0
  28. haadic/design/components/ekv_bench_ac.cir +12 -0
  29. haadic/design/layouts/__init__.py +5 -0
  30. haadic/design/layouts/active.py +125 -0
  31. haadic/design/layouts/base_cell.py +309 -0
  32. haadic/design/layouts/commun_source.py +32 -0
  33. haadic/design/layouts/general.py +109 -0
  34. haadic/design/layouts/inductor.py +166 -0
  35. haadic/design/layouts/microstrip.py +262 -0
  36. haadic/design/layouts/tools.py +28 -0
  37. haadic/design/models/__init__.py +1 -0
  38. haadic/design/models/constants.py +4 -0
  39. haadic/design/models/filter.py +87 -0
  40. haadic/design/models/inductor.py +33 -0
  41. haadic/design/models/matching.py +185 -0
  42. haadic/design/models/micro_strip.py +41 -0
  43. haadic/design/models/tools.py +74 -0
  44. haadic/design/post_processors/__init__.py +5 -0
  45. haadic/design/post_processors/ekv.py +239 -0
  46. haadic/design/post_processors/graphs.py +66 -0
  47. haadic/io/__init__.py +5 -0
  48. haadic/io/handlers/__init__.py +1 -0
  49. haadic/io/handlers/netlist.py +228 -0
  50. haadic/io/readers/__init__.py +1 -0
  51. haadic/io/readers/layermap.lark +16 -0
  52. haadic/io/readers/layermap.py +90 -0
  53. haadic/io/readers/magic_tech.py +55 -0
  54. haadic/io/readers/process.lark +22 -0
  55. haadic/io/readers/process.py +139 -0
  56. haadic/io/readers/raw.py +73 -0
  57. haadic/io/readers/tlef.lark +40 -0
  58. haadic/io/readers/tlef.py +205 -0
  59. haadic/io/readers/tools.py +16 -0
  60. haadic/io/wrappers/__init__.py +10 -0
  61. haadic/io/wrappers/em.py +141 -0
  62. haadic/io/wrappers/klayout.py +107 -0
  63. haadic/io/wrappers/magic.py +92 -0
  64. haadic/io/wrappers/magic_extract.tcl +22 -0
  65. haadic/io/wrappers/ngspice.py +70 -0
  66. haadic/io/wrappers/oems.py +264 -0
  67. haadic/io/wrappers/shell.nix +13 -0
  68. haadic/io/wrappers/tools.py +65 -0
  69. haadic/io/writers/__init__.py +1 -0
  70. haadic/io/writers/haadicfile.py +383 -0
  71. haadic/main.py +80 -0
  72. haadic-0.6.0.dist-info/METADATA +131 -0
  73. haadic-0.6.0.dist-info/RECORD +75 -0
  74. haadic-0.6.0.dist-info/WHEEL +4 -0
  75. haadic-0.6.0.dist-info/entry_points.txt +3 -0
haadic/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """
2
+ A Python framework for analog IC design automation.
3
+
4
+ # Haadic structure:
5
+
6
+ - core
7
+ - flow: for defining and running design flows
8
+ - steps: for defining the core steps in the design flow, such as layout generation, extraction, simulation, etc.
9
+ - techno: for handling technology-specific information and operations.
10
+ - io
11
+ - readers: files readers (spice, raw, etc.)
12
+ - writers: files writers (spice)
13
+ - wrappers: for external tools (e.g., magic, netgen, etc.)
14
+ - design
15
+ - layouts: for layout generation and manipulation
16
+ - models: for device and circuit modeling
17
+ - evaluators: for performance evaluation
18
+ - components: parameterized components (e.g., common sources) and design helpers (e.g., EKV model).
19
+ """
20
+
21
+ name = "haadic"
haadic/_config.py ADDED
@@ -0,0 +1,5 @@
1
+ from pathlib import Path
2
+
3
+ ROOT_DIR = Path(__file__).parent
4
+ DATA_DIR = ROOT_DIR / "data"
5
+ REF_PATH = ROOT_DIR.parent / "tests" / "ref_files"
@@ -0,0 +1 @@
1
+ """Module for the haadic core functionality: run analog flow."""
haadic/core/flow.py ADDED
@@ -0,0 +1,111 @@
1
+ """Module defining the Flow class, which composes multiple steps (layout generation, extraction, simulation, post-processing) into a complete design flow."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Callable, Iterable
6
+ from dataclasses import dataclass, field
7
+ from tabulate import tabulate
8
+
9
+ import haadic.core.steps.step as step
10
+ from haadic.core.steps.extraction import Extract, ConfigExtract
11
+ from haadic.core.steps.layout_generation import Layout, ConfigLayout, LayoutFunc
12
+ from haadic.core.steps.spice_simulation import BenchSim, ConfigSim
13
+ from haadic.core.steps.post_process import PostProcess, ConfigPostProc, PostProcessFunc
14
+ from haadic.core.techno import Available_PDK
15
+ from haadic.io.wrappers.magic import ExtractLevels
16
+
17
+
18
+ @dataclass
19
+ class ConfigFlow:
20
+ """
21
+ Configuration for a Flow execution.
22
+
23
+ :param techno: Selected PDK name.
24
+ :param reload: Whether to reload intermediate files when available (e.g., layout, spice netlist) or to recompute them.
25
+ :param run_dir: Base directory where results are written.
26
+ :param extract_level: Extraction level used by the Extract step.
27
+ :param sweep_folder: If True, create a separate folder per sweep value when using the run_from_* methods.
28
+ """
29
+
30
+ techno: Available_PDK = "sky130"
31
+ reload: bool = True
32
+ run_dir: Path = Path("./results")
33
+ extract_level: ExtractLevels = "RC"
34
+ sweep_folder: bool = True
35
+
36
+
37
+ @dataclass
38
+ class Flow:
39
+ """
40
+ Flow dataclass.
41
+
42
+ :param layout: function that generates the layout. It takes as argument a klayout Cell, a LayerStack and the dimensions of the layout to generate.
43
+ It should return a klayout Cell with the generated layout.
44
+ :param benches: list of bench files to run. The flow will look for these files in the current folder and run them with the extracted spice netlist.
45
+ They can be absolute or relative to the running folder.
46
+ :param postprocess: function that evaluates the performances of the circuit. It takes as argument the simulation results of the benches and the dimensions of the layout.
47
+ It should return a dictionary with the performance metrics to optimize as keys and their values as values.
48
+ :param config: flow options, such as the technology to use, whether to reload the layout if it already exists, and the folder to run the flow in.
49
+ """
50
+
51
+ layout: LayoutFunc
52
+ benches: Iterable[Path]
53
+ postprocess: Iterable[PostProcessFunc]
54
+ config: ConfigFlow = field(default_factory=ConfigFlow)
55
+
56
+ def run_from_dim(self, dimensions: step.Dim) -> step.Dim:
57
+ """
58
+ Run the composed flow starting from explicit dimensions.
59
+
60
+ Args:
61
+ dimensions: a `Dim` instance describing layout dimensions/parameters.
62
+
63
+ Returns:
64
+ A `Dim`-like object containing aggregated performance metrics produced by the postprocess steps.
65
+
66
+ """
67
+ datas = step.Dim()
68
+ d_benches = [f.resolve() for f in self.benches]
69
+ for bench, eval in zip(d_benches, self.postprocess):
70
+ start = step.init_step(
71
+ dimensions, self.config.run_dir, self.config.sweep_folder
72
+ )
73
+ flow = step.compose(
74
+ Layout(ConfigLayout(self.layout, self.config.techno)),
75
+ Extract(ConfigExtract(self.config.techno, self.config.extract_level)),
76
+ BenchSim(ConfigSim(bench, self.config.techno)),
77
+ reload=self.config.reload,
78
+ )
79
+ output_file = flow.run(start)
80
+ pp = PostProcess(ConfigPostProc(eval))
81
+ data = pp.run(output_file, dimensions)
82
+ for key in data.dct:
83
+ datas[key] = data[key]
84
+ return datas
85
+
86
+ def run_from_target(
87
+ self,
88
+ target: step.Dim,
89
+ local_model: Callable[[step.Dim], step.Dim],
90
+ ) -> step.Dim:
91
+ """
92
+ Run the flow from a target specification using a local model.
93
+
94
+ Args:
95
+ target: A `Dim` describing desired target values.
96
+ local_model: Callable that maps the `target` to actual layout `Dim` parameters.
97
+
98
+ Returns:
99
+ A `Dim`-like object containing aggregated performance metrics produced by the postprocess steps.
100
+
101
+ """
102
+ perf = self.run_from_dim(local_model(target))
103
+ res = [(key, perf.dct[key], target.dct.get(key, "N/A")) for key in perf.dct]
104
+ logging.info(
105
+ "\n" + tabulate(res, headers=["obtained", "targeted"], tablefmt="grid")
106
+ )
107
+
108
+ logging.info("compare performances to targets")
109
+ cost = step.compare_to(perf.dct, target.dct)
110
+ logging.info(f"current cost: {cost}")
111
+ return perf
@@ -0,0 +1 @@
1
+ """Module for the core steps in the design flow."""
@@ -0,0 +1,53 @@
1
+ """Module defining the Extract step, which performs layout extraction to generate a SPICE netlist from a GDSII layout."""
2
+
3
+ from haadic.core.steps.step import Step
4
+ from dataclasses import dataclass, field
5
+ from typing import Sequence
6
+ from haadic.io.wrappers.magic import ExtractLevels, extract_spice
7
+ from pathlib import Path
8
+ from haadic.core.techno import get_file, Available_PDK
9
+
10
+
11
+ @dataclass
12
+ class ConfigExtract:
13
+ """
14
+ Configuration for the Extract step.
15
+
16
+ :param techno: technology to use for extraction (e.g., "sky130", "gf180mcu").
17
+ :param level: extraction level to use (e.g., "NoPar", "RC").
18
+ """
19
+
20
+ techno: Available_PDK = "sky130"
21
+ level: ExtractLevels = "RC"
22
+
23
+
24
+ @dataclass
25
+ class Extract(Step):
26
+ """
27
+ Extract step dataclass.
28
+
29
+ :param config: configuration for the Extract step, including the technology to use and the extraction level.
30
+ :param input_suffixes: list of suffixes for the expected input file(s) (default: [".gds"]).
31
+ :param output_suffix: suffix for the output file (default: ".cir").
32
+ """
33
+
34
+ config: ConfigExtract = field(default_factory=ConfigExtract)
35
+ input_suffixes: Sequence[str] = field(default_factory=lambda: [".gds"])
36
+ output_suffix: str = ".cir"
37
+
38
+ def run(self, input_file: Path = Path("top.gds")) -> Path:
39
+ """
40
+ Extract a netlist from a given layout.
41
+
42
+ :param Path input_file: input gds file.
43
+ :return Path: extracted spice circuit.
44
+ """
45
+ output_path = self.output_file(input_file)
46
+ rc_file = get_file(self.config.techno, "magic_rc")
47
+ return extract_spice(
48
+ input_file,
49
+ rc_file,
50
+ input_file.stem,
51
+ output_path,
52
+ options=self.config.level,
53
+ )
@@ -0,0 +1,59 @@
1
+ """Module defining the Layout step for generating layouts from parametric descriptions."""
2
+
3
+ import logging
4
+ from haadic.core.techno import Available_PDK
5
+ from pathlib import Path
6
+ from typing import Callable, Sequence
7
+ from dataclasses import field, dataclass
8
+ import json
9
+
10
+ from haadic.design.layouts.base_cell import BaseCell
11
+ from haadic.core.steps.step import Dim, Step
12
+
13
+ type LayoutFunc = Callable[[BaseCell, Dim], BaseCell]
14
+
15
+
16
+ @dataclass
17
+ class ConfigLayout:
18
+ """
19
+ Configuration for the Layout step.
20
+
21
+ :param layout: function that generates the layout.
22
+ :param techno: technology to be used.
23
+ """
24
+
25
+ layout: LayoutFunc
26
+ techno: Available_PDK = "sky130"
27
+
28
+
29
+ @dataclass
30
+ class Layout(Step):
31
+ """
32
+ Step to generate a layout from a parametric description.
33
+
34
+ :param config: configuration for the layout generation step, including the layout function and the technology to use.
35
+ :param input_suffixes: list of expected suffixes for the input files. By default, it is set to [".json"].
36
+ :param output_suffix: suffix for the output file. By default, it is set to ".gds".
37
+ """
38
+
39
+ config: ConfigLayout
40
+ input_suffixes: Sequence[str] = field(default_factory=lambda: [".json"])
41
+ output_suffix: str = ".gds"
42
+
43
+ def run(self, input_file: Path) -> Path:
44
+ """
45
+ Generate a layout in the requested techologies with the given parametric layout and the given set of parameters.
46
+
47
+ :param input_file: set of parameters for the layout.
48
+ """
49
+ with input_file.open() as f:
50
+ dimensions = json.load(f)
51
+ output_file = self.output_file(input_file)
52
+ logging.info("layout generation with geometry: " + str(dimensions))
53
+
54
+ cellb = BaseCell(self.config.techno, input_file.stem)
55
+
56
+ self.config.layout(cellb, dimensions)
57
+
58
+ cellb.write(output_file)
59
+ return output_file
@@ -0,0 +1,58 @@
1
+ """Module defining the PostProcess step for post-processing simulation results and extracting performance metrics."""
2
+
3
+ from haadic.io.readers.raw import parse_out
4
+ from typing import Callable, Sequence
5
+ from dataclasses import field, dataclass
6
+ from pathlib import Path
7
+ import pandas as pd
8
+
9
+ from haadic.core.steps.step import Dim
10
+
11
+ SimRes = pd.DataFrame
12
+
13
+ type PostProcessFunc = Callable[[SimRes, Dim, Path], Dim]
14
+
15
+
16
+ @dataclass
17
+ class ConfigPostProc:
18
+ """
19
+ Configuration for the PostProcess step.
20
+
21
+ :param evaluate: function that evaluates the performances of the circuit.
22
+ It takes as argument the simulation results of the benches and the dimensions of the layout.
23
+ It should return a Dim class with the performance metrics.
24
+ """
25
+
26
+ evaluate: PostProcessFunc
27
+
28
+
29
+ @dataclass
30
+ class PostProcess:
31
+ """
32
+ Step to post-process the simulation results and extract performance metrics.
33
+
34
+ This step is meant to be run after a spice simulation step, and it takes as input the raw output file of the simulation (with suffix .raw)
35
+ and produces as output a Dim class with the performance metrics.
36
+
37
+ :param config: configuration for the post-processing step.
38
+ :param input_suffixes: list of expected suffixes for the input files. By default, it is set to [".raw"].
39
+ :param output_suffix: suffix for the output file. By default, it is set to "".
40
+ """
41
+
42
+ config: ConfigPostProc
43
+ input_suffixes: Sequence[str] = field(default_factory=lambda: [".raw"])
44
+ output_suffix: str = ""
45
+
46
+ def run(self, data_file: Path = Path("top.raw"), dimensions: Dim = Dim()) -> Dim:
47
+ """
48
+ Run the post-processing step.
49
+
50
+ This method reads the raw simulation output file, evaluates the performance metrics using the provided evaluate function, and returns a Dim class with the performance metrics.
51
+
52
+ :param data_file: path to the raw simulation output file.
53
+ :param dimensions: dimensions of the layout.
54
+ :return Dim: performance metrics.
55
+ """
56
+ data = parse_out(data_file)
57
+ base_dir = data_file.parent
58
+ return self.config.evaluate(data, dimensions, base_dir)
@@ -0,0 +1,63 @@
1
+ """Spice simulation step for the haadic design flow."""
2
+
3
+ from typing import Sequence
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from haadic.io.handlers.netlist import Netlist
7
+ from haadic.io.wrappers.ngspice import compute
8
+ from haadic.io.wrappers.tools import to_wsl
9
+ from haadic.core.techno import get_file, load_pdk, Available_PDK
10
+ from haadic.core.steps.step import can_skip, Step
11
+
12
+
13
+ @dataclass
14
+ class ConfigSim:
15
+ """
16
+ Configuration for the BenchSim step.
17
+
18
+ :param bench: path to the bench file to simulate, defaults to "bench.cir"."
19
+ :param techno: name of the technology to use for the simulation, defaults to "sky130".
20
+ """
21
+
22
+ bench: Path = Path("bench.cir")
23
+ techno: Available_PDK = "sky130"
24
+
25
+
26
+ @dataclass
27
+ class BenchSim(Step):
28
+ """
29
+ Step to run a spice simulation using ngspice.
30
+
31
+ :param config: configuration for the BenchSim step.
32
+ :param input_suffixes: list of accepted input file suffixes for this step, defaults to [".cir"].
33
+ :param output_suffix: suffix for the output file generated by this step, defaults to ".raw".
34
+ """
35
+
36
+ config: ConfigSim = field(default_factory=ConfigSim)
37
+ input_suffixes: Sequence[str] = field(default_factory=lambda: [".cir"])
38
+ output_suffix: str = ".raw"
39
+
40
+ def run(self, input_file: Path = Path("top.cir")) -> Path:
41
+ """
42
+ Run the given netlist using the requested technologie.
43
+
44
+ :param input_file: spice netlist bench to be simulated, defaults to "bench.cir"
45
+ :return Path: path to the output raw file generated by ngspice.
46
+ """
47
+ bench = self.config.bench
48
+ data_file = (
49
+ Path(input_file).with_name(bench.name).with_suffix(self.output_suffix)
50
+ )
51
+ if can_skip(bench, data_file) and can_skip(input_file, data_file):
52
+ return data_file
53
+ techno = self.config.techno
54
+
55
+ spice = Netlist().load(bench)
56
+ section = load_pdk(techno).get("section", "tt")
57
+ spice.add_lib(to_wsl(get_file(techno, "lib_spice")), section)
58
+
59
+ spice.add_include(input_file)
60
+ spice.write(data_file.with_name(bench.name))
61
+
62
+ compute(data_file.with_name(bench.name), data_file)
63
+ return data_file
@@ -0,0 +1,226 @@
1
+ """Module defining the Step protocol and related utilities for managing steps in the haadic flow."""
2
+
3
+ import json
4
+ from functools import reduce
5
+ from typing import Any, Protocol, Sequence
6
+ import logging
7
+ import os
8
+ from pathlib import Path
9
+ import shutil
10
+
11
+ import pydantic
12
+
13
+
14
+ @pydantic.dataclasses.dataclass
15
+ class Dim:
16
+ """
17
+ Simple container for named dimension/performance values.
18
+
19
+ Stores a mapping of string keys to float values and provides convenient
20
+ accessors used throughout the flow (indexing, stringification).
21
+ """
22
+
23
+ dct: dict[str, float] = pydantic.Field(default_factory=dict)
24
+
25
+ def __getitem__(self, key: str) -> float:
26
+ """Get the value corresponding to the given key."""
27
+ return self.dct[key]
28
+
29
+ def __setitem__(self, key: str, value: float) -> None:
30
+ """Set the value corresponding to the given key."""
31
+ self.dct[key] = float(value)
32
+
33
+ def __str__(self) -> str:
34
+ """
35
+ Get a string representation of the dimensions, in the format "key1_value1__key2_value2".
36
+
37
+ This format is used for naming files and folders corresponding to specific dimension values.
38
+ """
39
+ return "__".join([f"{key}_{value:g}" for key, value in self.dct.items()])
40
+
41
+
42
+ class Step(Protocol):
43
+ """
44
+ Class storing information for a step.
45
+
46
+ Each step of a Flow should be implemented as a class inheriting from Step and implementing the run method, which takes an input file and produces an output file.
47
+ """
48
+
49
+ input_suffixes: Sequence[str]
50
+ output_suffix: str
51
+ config: Any
52
+
53
+ def output_file(self, input_file: Path) -> Path:
54
+ """Return the expected output file path for a given input file."""
55
+ return input_file.with_suffix(self.output_suffix)
56
+
57
+ def run(self, input_file: Path) -> Path:
58
+ """
59
+ Meta-method to be implemented by each step.
60
+
61
+ :param input_file: path to the input file for the step.
62
+ :return: path to the output file produced by the step.
63
+ """
64
+ pass
65
+
66
+
67
+ def init_step(dimensions: Dim, base_dir: Path, sweep_folder: bool = False) -> Path:
68
+ """
69
+ Create or locate the JSON file representing the initial step for a run.
70
+
71
+ The function writes `top.json` in `base_dir` (or in a subfolder named after
72
+ `dimensions` when `sweep_folder` is True). If an existing file contains the
73
+ same dimensions it is returned directly.
74
+
75
+ Args:
76
+ dimensions: a `Dim` instance with the design parameters.
77
+ base_dir: base run directory where `top.json` will be written.
78
+ sweep_folder: whether to place `top.json` in a subfolder named after `dimensions`.
79
+
80
+ Returns:
81
+ Path to the created or existing `top.json` file.
82
+
83
+ """
84
+ if sweep_folder:
85
+ output_file = base_dir / dimensions.__str__() / "top.json"
86
+ else:
87
+ output_file = base_dir / "top.json"
88
+ if output_file.is_file():
89
+ with output_file.open("r") as f:
90
+ ref = json.load(f)
91
+ if ref == dimensions.dct:
92
+ return output_file
93
+ if not output_file.parent.is_dir():
94
+ output_file.parent.mkdir(parents=True, exist_ok=True)
95
+ with output_file.open("w") as f:
96
+ json.dump(dimensions.dct, f)
97
+ return output_file
98
+
99
+
100
+ def validate_input(input_file: Path, valid_suffixes: Sequence[str]) -> None:
101
+ """
102
+ Raise a ValueError if `input_file` does not have an allowed suffix.
103
+
104
+ Args:
105
+ input_file: file to validate.
106
+ valid_suffixes: sequence of accepted suffix strings (including leading dot).
107
+
108
+ """
109
+ if input_file.suffix not in valid_suffixes:
110
+ raise ValueError(f"{input_file} suffix is not in {valid_suffixes}")
111
+
112
+
113
+ def can_skip(input_file: Path, output_file: Path):
114
+ """
115
+ Return True if the step can be skipped based on file modification times.
116
+
117
+ A step can be skipped when `output_file` exists and is newer than `input_file`.
118
+ """
119
+ if not output_file.is_file():
120
+ return False
121
+ if input_file.stat().st_mtime >= output_file.stat().st_mtime:
122
+ return False
123
+ return True
124
+
125
+
126
+ def compose(*steps: Step, reload: bool = True) -> Step:
127
+ """
128
+ Compose multiple Step implementations into a single Step instance.
129
+
130
+ The returned object implements the `run(input_file: Path) -> Path` method
131
+ which will execute each step in order, validating inputs and optionally
132
+ skipping steps when outputs are up-to-date (controlled by `reload`).
133
+ """
134
+
135
+ class Compose(Step):
136
+ input_suffixes: Sequence[str]
137
+ output_suffix: str
138
+ config: dict[str, Any]
139
+
140
+ def __init__(self, config: dict[str, Any]):
141
+ self.input_suffixes = steps[0].input_suffixes
142
+ self.output_suffix = steps[-1].output_suffix
143
+ self.config = config
144
+
145
+ def run(self, input_file: Path) -> Path:
146
+ """
147
+ Execute the composed steps sequentially starting from `input_file`.
148
+
149
+ Args:
150
+ input_file: initial input file for the first step.
151
+
152
+ Returns:
153
+ Path to the final step's output file.
154
+
155
+ """
156
+
157
+ def fun(path: Path, step: Step) -> Path:
158
+ validate_input(path, step.input_suffixes)
159
+ excepted_output = step.output_file(path)
160
+ if self.config["reload"] and can_skip(path, excepted_output):
161
+ logging.info(
162
+ f"Skipping step {step.__class__.__name__} as {excepted_output.name} is up to date with {path.name}"
163
+ )
164
+ return excepted_output
165
+ logging.info(
166
+ f"Running step {step.__class__.__name__} with input {path.name}"
167
+ )
168
+ return step.run(path)
169
+
170
+ return reduce(fun, steps, input_file)
171
+
172
+ return Compose({"reload": reload})
173
+
174
+
175
+ def cleanup(folder: str = "", dry_run: bool = False):
176
+ """
177
+ Remove all files generated by haadic in the specified folder.
178
+
179
+ :param folder: path of the folder to clean.
180
+ :param dry_run: if True, only print which files would be removed without actually removing them. Default is False.
181
+ """
182
+ suffix_to_remove = [".gds", ".cir", ".raw", ".log", ".nodes", ".sim", ".tcl"]
183
+ if folder:
184
+ os.chdir(Path(folder))
185
+ for suffix in suffix_to_remove:
186
+ file = Path("top").with_suffix(suffix)
187
+ if file.is_file():
188
+ if dry_run:
189
+ print(f"Would remove file: {file}")
190
+ else:
191
+ os.remove(file)
192
+ directories_to_remove = [
193
+ "extfile",
194
+ ]
195
+ for directory in directories_to_remove:
196
+ dir_path = Path(directory)
197
+ if dir_path.is_dir():
198
+ if dry_run:
199
+ print(f"Would remove directory: {dir_path}")
200
+ else:
201
+ shutil.rmtree(dir_path)
202
+
203
+
204
+ def compare_to(perf: dict, target: dict):
205
+ """
206
+ Compute a simple squared-error cost between `perf` and `target`.
207
+
208
+ Missing keys in `perf` are treated as zero and a warning is emitted.
209
+
210
+ Args:
211
+ perf: dictionary of obtained performance values.
212
+ target: dictionary of target performance values.
213
+
214
+ Returns:
215
+ Numeric cost (sum of squared errors).
216
+
217
+ """
218
+ cost = 0
219
+ for key in target:
220
+ if perf is None or key not in perf:
221
+ logging.warning(f"Key {key} not found in performance dictionary")
222
+ cost += target[key] ** 2
223
+ else:
224
+ cost += (target[key] - perf[key]) ** 2
225
+
226
+ return cost