monata 0.1.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 (135) hide show
  1. monata/__init__.py +35 -0
  2. monata/_config.py +105 -0
  3. monata/_home.py +47 -0
  4. monata/_json.py +57 -0
  5. monata/_paths.py +73 -0
  6. monata/_types.py +8 -0
  7. monata/category.py +139 -0
  8. monata/cell.py +179 -0
  9. monata/circuits/__init__.py +29 -0
  10. monata/circuits/cmos.py +102 -0
  11. monata/circuits/source.py +70 -0
  12. monata/corner.py +355 -0
  13. monata/eda/__init__.py +35 -0
  14. monata/eda/kicad.py +526 -0
  15. monata/errors.py +33 -0
  16. monata/examples.py +79 -0
  17. monata/generation/__init__.py +6 -0
  18. monata/generation/netlist.py +72 -0
  19. monata/generation/symbol.py +26 -0
  20. monata/library.py +323 -0
  21. monata/measure/__init__.py +55 -0
  22. monata/measure/calculus.py +330 -0
  23. monata/measure/freq_domain.py +250 -0
  24. monata/measure/result.py +107 -0
  25. monata/measure/spec.py +385 -0
  26. monata/measure/statistics.py +91 -0
  27. monata/measure/summary.py +217 -0
  28. monata/measure/time_domain.py +362 -0
  29. monata/models/__init__.py +32 -0
  30. monata/models/artifacts.py +104 -0
  31. monata/models/cache.py +355 -0
  32. monata/models/compiler.py +239 -0
  33. monata/models/diagnostics.py +46 -0
  34. monata/models/flow.py +243 -0
  35. monata/models/manifest.py +438 -0
  36. monata/models/registry.py +369 -0
  37. monata/models/resolver.py +970 -0
  38. monata/netlist/__init__.py +45 -0
  39. monata/netlist/device_schema.py +394 -0
  40. monata/netlist/ir.py +1026 -0
  41. monata/netlist/mutation.py +173 -0
  42. monata/netlist/ngspice.py +221 -0
  43. monata/netlist/scope_api.py +1601 -0
  44. monata/netlist/scope_state.py +137 -0
  45. monata/netlist/topology.py +369 -0
  46. monata/optim/__init__.py +13 -0
  47. monata/optim/base.py +155 -0
  48. monata/optim/bayesian.py +187 -0
  49. monata/optim/circuit.py +166 -0
  50. monata/optim/nsga2.py +272 -0
  51. monata/parser/__init__.py +92 -0
  52. monata/parser/analysis_import.py +1009 -0
  53. monata/parser/commands.py +70 -0
  54. monata/parser/deck.py +348 -0
  55. monata/parser/errors.py +28 -0
  56. monata/parser/expression.py +457 -0
  57. monata/parser/import_plan.py +687 -0
  58. monata/parser/importer.py +636 -0
  59. monata/parser/source_subcircuit.py +53 -0
  60. monata/physics.py +309 -0
  61. monata/projection.py +238 -0
  62. monata/py.typed +0 -0
  63. monata/registry.py +59 -0
  64. monata/sim/__init__.py +5 -0
  65. monata/sim/_backend.py +28 -0
  66. monata/sim/_digital_bits.py +42 -0
  67. monata/sim/_frozen.py +124 -0
  68. monata/sim/_vector_identity.py +93 -0
  69. monata/sim/analysis_spec.py +310 -0
  70. monata/sim/artifacts.py +150 -0
  71. monata/sim/backends/__init__.py +41 -0
  72. monata/sim/backends/base.py +335 -0
  73. monata/sim/backends/ngspice.py +415 -0
  74. monata/sim/backends/ngspice_common.py +550 -0
  75. monata/sim/backends/ngspice_output.py +321 -0
  76. monata/sim/backends/ngspice_parse.py +377 -0
  77. monata/sim/backends/ngspice_plan.py +621 -0
  78. monata/sim/backends/ngspice_shared.py +344 -0
  79. monata/sim/backends/ngspice_shared_commands.py +65 -0
  80. monata/sim/backends/ngspice_shared_ffi.py +130 -0
  81. monata/sim/backends/ngspice_shared_session.py +621 -0
  82. monata/sim/backends/ngspice_stdout.py +237 -0
  83. monata/sim/cache.py +367 -0
  84. monata/sim/capabilities.py +191 -0
  85. monata/sim/core.py +77 -0
  86. monata/sim/corner.py +261 -0
  87. monata/sim/digital_circuits.py +199 -0
  88. monata/sim/digital_claims.py +310 -0
  89. monata/sim/digital_extract.py +402 -0
  90. monata/sim/digital_plan.py +505 -0
  91. monata/sim/digital_projection.py +111 -0
  92. monata/sim/digital_results.py +186 -0
  93. monata/sim/digital_spec.py +161 -0
  94. monata/sim/digital_table.py +512 -0
  95. monata/sim/digital_table_config.py +234 -0
  96. monata/sim/digital_tasks.py +225 -0
  97. monata/sim/digital_timing.py +109 -0
  98. monata/sim/executor.py +100 -0
  99. monata/sim/export.py +65 -0
  100. monata/sim/export_hdf5.py +339 -0
  101. monata/sim/export_payload.py +296 -0
  102. monata/sim/montecarlo.py +251 -0
  103. monata/sim/plot.py +156 -0
  104. monata/sim/rawfile.py +403 -0
  105. monata/sim/result_ops.py +140 -0
  106. monata/sim/results.py +782 -0
  107. monata/sim/session.py +382 -0
  108. monata/sim/sweep.py +272 -0
  109. monata/sim/task.py +152 -0
  110. monata/sim/vector_names.py +203 -0
  111. monata/sim/waveform.py +785 -0
  112. monata/spice_library.py +775 -0
  113. monata/techlib/__init__.py +5 -0
  114. monata/techlib/parse.py +187 -0
  115. monata/techlib/projection.py +168 -0
  116. monata/techlib/registry.py +501 -0
  117. monata/techlib/schema.py +130 -0
  118. monata/units.py +1251 -0
  119. monata/views/__init__.py +5 -0
  120. monata/views/base.py +155 -0
  121. monata/views/digital_truth_table.py +280 -0
  122. monata/views/netlist.py +15 -0
  123. monata/views/registry.py +332 -0
  124. monata/views/schematic.py +10 -0
  125. monata/views/simulation.py +312 -0
  126. monata/views/symbol.py +64 -0
  127. monata/views/testbench.py +16 -0
  128. monata/workspace/__init__.py +6 -0
  129. monata/workspace/experiment.py +93 -0
  130. monata/workspace/project.py +230 -0
  131. monata/workspace/result_store.py +177 -0
  132. monata-0.1.0.dist-info/METADATA +142 -0
  133. monata-0.1.0.dist-info/RECORD +135 -0
  134. monata-0.1.0.dist-info/WHEEL +4 -0
  135. monata-0.1.0.dist-info/licenses/LICENSE +21 -0
monata/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """Monata — a lightweight EDA framework for circuit design."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from monata.cell import Cell
6
+ from monata.category import Category
7
+ from monata.corner import OperatingCorner
8
+ from monata.errors import (
9
+ CellNotFoundError,
10
+ LibraryNotFoundError,
11
+ ViewAlreadyModifiedError,
12
+ ViewNotFoundError,
13
+ ViewNotGeneratedError,
14
+ )
15
+ from monata.library import Library
16
+ from monata.registry import LibraryRegistry
17
+ from monata.units import Quantity, Unit, UnitArray
18
+ from monata.views import View
19
+
20
+ __all__ = [
21
+ "Cell",
22
+ "CellNotFoundError",
23
+ "Category",
24
+ "Library",
25
+ "LibraryNotFoundError",
26
+ "LibraryRegistry",
27
+ "OperatingCorner",
28
+ "Quantity",
29
+ "Unit",
30
+ "UnitArray",
31
+ "View",
32
+ "ViewAlreadyModifiedError",
33
+ "ViewNotFoundError",
34
+ "ViewNotGeneratedError",
35
+ ]
monata/_config.py ADDED
@@ -0,0 +1,105 @@
1
+ """Internal TOML persistence helpers for Monata workspace metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ try:
10
+ import tomllib
11
+ except ImportError:
12
+ import tomli as tomllib # type: ignore[no-redef]
13
+
14
+ from monata._paths import toml_string
15
+
16
+
17
+ def read_toml(path: Path) -> dict[str, Any]:
18
+ with path.open("rb") as file:
19
+ return tomllib.load(file)
20
+
21
+
22
+ def reject_unknown_fields(
23
+ data: Mapping[str, Any], allowed: frozenset[str], subject: str
24
+ ) -> None:
25
+ unknown = sorted(key for key in data if key not in allowed)
26
+ if unknown:
27
+ raise ValueError(f"{subject} has unknown fields: {', '.join(unknown)}")
28
+
29
+
30
+ def cell_config(name: str, *, description: str = "") -> dict[str, Any]:
31
+ return {"cell": {"name": name, "description": description}, "views": {}}
32
+
33
+
34
+ def category_config(name: str, *, description: str = "") -> dict[str, Any]:
35
+ return {"category": {"name": name, "description": description}}
36
+
37
+
38
+ def write_category_config(path: Path, config: Mapping[str, Any]) -> None:
39
+ category = config["category"]
40
+ lines = [f'[category]\nname = "{toml_string(category["name"])}"\n']
41
+ if "description" in category:
42
+ lines.append(f'description = "{toml_string(category["description"])}"\n')
43
+ path.write_text("".join(lines))
44
+
45
+
46
+ def write_cell_config(path: Path, config: Mapping[str, Any]) -> None:
47
+ cell = config["cell"]
48
+ lines = [f'[cell]\nname = "{toml_string(cell["name"])}"\n']
49
+ if "description" in cell:
50
+ lines.append(f'description = "{toml_string(cell["description"])}"\n')
51
+ lines.append("\n[views]\n")
52
+ for view_type, view_config in config.get("views", {}).items():
53
+ parts = ", ".join(f"{key} = {toml_value(value)}" for key, value in view_config.items())
54
+ lines.append(f"{view_type} = {{ {parts} }}\n")
55
+ path.write_text("".join(lines))
56
+
57
+
58
+ def write_library_config(
59
+ path: Path,
60
+ *,
61
+ name: str,
62
+ tech_model_paths: Iterable[Any],
63
+ description: str = "",
64
+ techlib_attachments: Iterable[str] = (),
65
+ default_corner: str | None = None,
66
+ ) -> None:
67
+ model_paths = ", ".join(toml_value(model_path) for model_path in tech_model_paths)
68
+ lines = [
69
+ f'[library]\nname = "{toml_string(name)}"\n',
70
+ f'description = "{toml_string(description)}"\n\n',
71
+ f"[technology]\nmodel_paths = [{model_paths}]\n",
72
+ ]
73
+ attachments = list(techlib_attachments)
74
+ if attachments or default_corner is not None:
75
+ techlibs = ", ".join(toml_value(attachment) for attachment in attachments)
76
+ lines.extend(["\n[attachments]\n", f"techlibs = [{techlibs}]\n"])
77
+ if default_corner is not None:
78
+ lines.append(f'default_corner = "{toml_string(default_corner)}"\n')
79
+ path.write_text("".join(lines))
80
+
81
+
82
+ def write_project_config(
83
+ path: Path,
84
+ *,
85
+ name: str,
86
+ libraries: Iterable[Mapping[str, str]] = (),
87
+ ) -> None:
88
+ lines = [f'[project]\nname = "{toml_string(name)}"\n']
89
+ for entry in libraries:
90
+ lines.extend([
91
+ "\n[[libraries]]\n",
92
+ f'name = "{toml_string(entry["name"])}"\n',
93
+ f'path = "{toml_string(entry["path"])}"\n',
94
+ ])
95
+ path.write_text("".join(lines))
96
+
97
+
98
+ def toml_value(value: Any) -> str:
99
+ if isinstance(value, bool):
100
+ return "true" if value else "false"
101
+ if isinstance(value, (int, float)):
102
+ return repr(value)
103
+ if isinstance(value, (list, tuple)):
104
+ return "[" + ", ".join(toml_value(item) for item in value) + "]"
105
+ return f'"{toml_string(value)}"'
monata/_home.py ADDED
@@ -0,0 +1,47 @@
1
+ """Monata-managed mutable state locations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def monata_home(explicit: str | Path | None = None) -> Path | None:
10
+ """Return the configured Monata home root, if one is configured."""
11
+
12
+ if explicit is not None:
13
+ return Path(explicit)
14
+ env = os.environ.get("MONATA_HOME")
15
+ return Path(env) if env else None
16
+
17
+
18
+ def default_user_cache_root() -> Path:
19
+ """Return the platform-neutral user cache root used when MONATA_HOME is unset."""
20
+
21
+ xdg = os.environ.get("XDG_CACHE_HOME")
22
+ if xdg:
23
+ return Path(xdg)
24
+ return Path(os.path.expanduser("~/.cache"))
25
+
26
+
27
+ def monata_cache_dir(*, home: str | Path | None = None) -> Path:
28
+ """Return the generic Monata cache directory for a configured home/default."""
29
+
30
+ root = monata_home(home)
31
+ if root is not None:
32
+ return root / "cache"
33
+ return default_user_cache_root() / "monata"
34
+
35
+
36
+ def monata_registry_dir(*, home: str | Path | None = None) -> Path | None:
37
+ """Return the optional Monata registry directory when a home root exists."""
38
+
39
+ root = monata_home(home)
40
+ return root / "registry" if root is not None else None
41
+
42
+
43
+ def monata_logs_dir(*, home: str | Path | None = None) -> Path | None:
44
+ """Return the optional Monata logs directory when a home root exists."""
45
+
46
+ root = monata_home(home)
47
+ return root / "logs" if root is not None else None
monata/_json.py ADDED
@@ -0,0 +1,57 @@
1
+ """Shared JSON-safe value coercion helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+
12
+
13
+ def json_safe(value: Any, *, strict: bool = False) -> Any:
14
+ """Return a recursively JSON-compatible representation of common Monata values."""
15
+
16
+ result = _json_safe(value)
17
+ if strict:
18
+ json.dumps(result)
19
+ return result
20
+
21
+
22
+ def json_safe_dict(values: Mapping[str, Any]) -> dict[str, Any]:
23
+ return {str(key): json_safe(value) for key, value in dict(values).items()}
24
+
25
+
26
+ def _json_safe(value: Any) -> Any:
27
+ if hasattr(value, "to_dict"):
28
+ return json_safe(value.to_dict())
29
+ if value is None or isinstance(value, str | int | float | bool):
30
+ return value
31
+ if isinstance(value, Path):
32
+ return str(value)
33
+ if isinstance(value, np.generic):
34
+ return json_safe(value.item())
35
+ if isinstance(value, complex):
36
+ return {"real": value.real, "imag": value.imag}
37
+ if isinstance(value, np.ndarray):
38
+ if np.iscomplexobj(value):
39
+ return {
40
+ "real": np.real(value).tolist(),
41
+ "imag": np.imag(value).tolist(),
42
+ }
43
+ return json_safe(value.tolist())
44
+ if isinstance(value, Mapping):
45
+ return json_safe_dict(value)
46
+ if isinstance(value, list | tuple):
47
+ return [json_safe(item) for item in value]
48
+ if isinstance(value, set | frozenset):
49
+ return [json_safe(item) for item in sorted(value, key=str)]
50
+ if hasattr(value, "item"):
51
+ try:
52
+ return json_safe(value.item())
53
+ except ValueError:
54
+ pass
55
+ if hasattr(value, "tolist"):
56
+ return json_safe(value.tolist())
57
+ return value
monata/_paths.py ADDED
@@ -0,0 +1,73 @@
1
+ """Shared path/name validation helpers for filesystem-backed APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from collections.abc import Iterable, Iterator
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ _SAFE_PATH_SEGMENT_RE = re.compile(r"^[A-Za-z0-9._-]+$")
13
+
14
+
15
+ def validate_path_segment(value: Any, label: str = "name") -> str:
16
+ """Return a safe single path segment or raise a stable ValueError."""
17
+
18
+ if value is None:
19
+ raise ValueError(f"{label} must be a single safe path segment: {value}")
20
+ text = str(value)
21
+ path = Path(text)
22
+ if (
23
+ not text
24
+ or _SAFE_PATH_SEGMENT_RE.match(text) is None
25
+ or path.is_absolute()
26
+ or len(path.parts) != 1
27
+ or text in {".", ".."}
28
+ ):
29
+ raise ValueError(f"{label} must be a single safe path segment: {text}")
30
+ return text
31
+
32
+
33
+ def expand_path(path: str | Path) -> Path:
34
+ """Return a path with environment variables and user markers expanded."""
35
+
36
+ return Path(os.path.expandvars(str(path))).expanduser().absolute()
37
+
38
+
39
+ def walk_files(path: str | Path, *, follow_symlinks: bool = False) -> Iterator[Path]:
40
+ """Yield files below a path in deterministic traversal order."""
41
+
42
+ root = Path(path)
43
+ if root.is_file():
44
+ yield root
45
+ return
46
+
47
+ for current_root, dirnames, filenames in os.walk(root, followlinks=follow_symlinks):
48
+ dirnames.sort()
49
+ for filename in sorted(filenames):
50
+ yield Path(current_root) / filename
51
+
52
+
53
+ def find_file(file_name: str | Path, directories: Iterable[str | Path]) -> Path:
54
+ """Return the first matching file name found under the provided directories."""
55
+
56
+ name = str(file_name)
57
+ if not name or Path(name).name != name:
58
+ raise ValueError(f"file_name must be a file name: {file_name}")
59
+
60
+ roots = tuple(directories)
61
+ for directory in roots:
62
+ for path in walk_files(directory):
63
+ if path.name == name:
64
+ return path
65
+
66
+ searched = ", ".join(str(directory) for directory in roots) or "<none>"
67
+ raise FileNotFoundError(f"file {name!r} not found under: {searched}")
68
+
69
+
70
+ def toml_string(value: Any) -> str:
71
+ """Escape a value for a basic TOML string."""
72
+
73
+ return str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\t", "\\t")
monata/_types.py ADDED
@@ -0,0 +1,8 @@
1
+ """Lightweight internal type aliases shared across public owner modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ ReferenceMode = Literal["concrete", "logical"]
8
+ NetlistProjectionMode = Literal["none", "concrete", "logical"]
monata/category.py ADDED
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from monata._config import (
8
+ category_config,
9
+ cell_config,
10
+ read_toml,
11
+ reject_unknown_fields,
12
+ write_category_config,
13
+ write_cell_config,
14
+ )
15
+ from monata._paths import validate_path_segment
16
+ from monata.errors import CellNotFoundError
17
+
18
+ CATEGORY_CONFIG_FILENAME = "category.toml"
19
+ CELL_CONFIG_FILENAME = "cell.toml"
20
+ _CATEGORY_CONFIG_FIELDS = frozenset({"category"})
21
+ _CATEGORY_TABLE_FIELDS = frozenset({"name", "description"})
22
+
23
+
24
+ def _validate_category_config(config: Mapping[str, Any]) -> None:
25
+ reject_unknown_fields(config, _CATEGORY_CONFIG_FIELDS, "category.toml")
26
+ try:
27
+ category = config["category"]
28
+ except KeyError as exc:
29
+ raise ValueError("category.toml is missing [category]") from exc
30
+ if not isinstance(category, Mapping):
31
+ raise ValueError("[category] must be a table")
32
+ reject_unknown_fields(category, _CATEGORY_TABLE_FIELDS, "category table")
33
+
34
+
35
+ class Category:
36
+ """Filesystem-backed library category containing cells and subcategories."""
37
+
38
+ def __init__(self, path, library):
39
+ self._path = Path(path)
40
+ self._library = library
41
+ self._config = None
42
+ self._cells_cache = None
43
+ self._categories_cache = None
44
+
45
+ @classmethod
46
+ def create(cls, path, library, *, name: str, description: str = "") -> "Category":
47
+ safe_name = validate_path_segment(name, "category name")
48
+ category_dir = Path(path)
49
+ category_dir.mkdir(parents=True, exist_ok=False)
50
+ write_category_config(
51
+ category_dir / CATEGORY_CONFIG_FILENAME,
52
+ category_config(safe_name, description=description),
53
+ )
54
+ return cls(category_dir, library)
55
+
56
+ def _load_config(self):
57
+ if self._config is None:
58
+ self._config = read_toml(self._path / CATEGORY_CONFIG_FILENAME)
59
+ _validate_category_config(self._config)
60
+ return self._config
61
+
62
+ @property
63
+ def path(self) -> Path:
64
+ return self._path
65
+
66
+ @property
67
+ def library(self):
68
+ return self._library
69
+
70
+ @property
71
+ def name(self) -> str:
72
+ return self._load_config()["category"]["name"]
73
+
74
+ @property
75
+ def qualified_name(self) -> str:
76
+ return self.path_relative_to_library().as_posix()
77
+
78
+ def path_relative_to_library(self) -> Path:
79
+ return self._path.relative_to(self._library.path)
80
+
81
+ def _cell_path(self, name: str) -> tuple[str, Path]:
82
+ safe_name = validate_path_segment(name, "cell name")
83
+ return safe_name, self._path / safe_name
84
+
85
+ def _scan_cells(self) -> dict[str, Path]:
86
+ if self._cells_cache is None:
87
+ self._cells_cache = {}
88
+ for entry in self._path.iterdir():
89
+ if entry.is_dir() and (entry / CELL_CONFIG_FILENAME).exists():
90
+ self._cells_cache[entry.name] = entry
91
+ return self._cells_cache
92
+
93
+ def _scan_categories(self) -> dict[str, Path]:
94
+ if self._categories_cache is None:
95
+ self._categories_cache = {}
96
+ for entry in self._path.iterdir():
97
+ if entry.is_dir() and (entry / CATEGORY_CONFIG_FILENAME).exists():
98
+ self._categories_cache[entry.name] = entry
99
+ return self._categories_cache
100
+
101
+ def list_cells(self) -> list[str]:
102
+ return sorted(self._scan_cells())
103
+
104
+ def list_categories(self) -> list[str]:
105
+ return sorted(self._scan_categories())
106
+
107
+ def get_category(self, path: str) -> "Category":
108
+ return self._library.get_category(f"{self.qualified_name}/{path}")
109
+
110
+ def create_category(self, name: str, description: str = "") -> "Category":
111
+ category = self._library.create_category(f"{self.qualified_name}/{name}", description=description)
112
+ self._categories_cache = None
113
+ return category
114
+
115
+ def create_cell(self, name: str, description: str = ""):
116
+ from monata.cell import Cell
117
+
118
+ safe_name, cell_dir = self._cell_path(name)
119
+ if cell_dir.exists():
120
+ raise FileExistsError(f"Cell already exists: {self.qualified_name}/{safe_name}")
121
+ cell_dir.mkdir()
122
+ write_cell_config(cell_dir / CELL_CONFIG_FILENAME, cell_config(safe_name, description=description))
123
+ self._cells_cache = None
124
+ self._library._clear_cell_cache()
125
+ return Cell(cell_dir, self._library, category=self)
126
+
127
+ def __getitem__(self, name: str):
128
+ from monata.cell import Cell
129
+
130
+ cells = self._scan_cells()
131
+ if name not in cells:
132
+ raise CellNotFoundError(name, self._library.name)
133
+ return Cell(cells[name], self._library, category=self)
134
+
135
+ def __contains__(self, name: str) -> bool:
136
+ return name in self._scan_cells()
137
+
138
+ def __iter__(self):
139
+ return iter(self._scan_cells())
monata/cell.py ADDED
@@ -0,0 +1,179 @@
1
+ from collections.abc import Mapping
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from monata._config import read_toml, reject_unknown_fields, write_cell_config
6
+ from monata._paths import validate_path_segment
7
+ from monata._types import NetlistProjectionMode
8
+ from monata.errors import ViewAlreadyModifiedError, ViewNotFoundError
9
+ from monata.views.registry import (
10
+ ViewConfig,
11
+ create_registered_view,
12
+ create_registered_view_config,
13
+ generate_registered_view,
14
+ )
15
+
16
+ _CELL_CONFIG_FIELDS = frozenset({"cell", "views"})
17
+ _CELL_TABLE_FIELDS = frozenset({"name", "description"})
18
+
19
+
20
+ def _validate_cell_config(config: Mapping[str, Any]) -> None:
21
+ reject_unknown_fields(config, _CELL_CONFIG_FIELDS, "cell.toml")
22
+ try:
23
+ cell = config["cell"]
24
+ except KeyError as exc:
25
+ raise ValueError("cell.toml is missing [cell]") from exc
26
+ if not isinstance(cell, Mapping):
27
+ raise ValueError("[cell] must be a table")
28
+ reject_unknown_fields(cell, _CELL_TABLE_FIELDS, "cell table")
29
+ views = config.get("views", {})
30
+ if not isinstance(views, Mapping):
31
+ raise ValueError("[views] must be a table")
32
+ for view_type, view_config in views.items():
33
+ safe_view_type = validate_path_segment(view_type, "view type")
34
+ if not isinstance(view_config, Mapping):
35
+ raise ValueError(f"view {safe_view_type} config must be a table")
36
+
37
+
38
+ class Cell:
39
+ def __init__(self, path, library, *, category=None):
40
+ self._path = Path(path)
41
+ self._library = library
42
+ self._category = category
43
+ self._config = None
44
+
45
+ def _load_config(self):
46
+ if self._config is None:
47
+ self._config = read_toml(self._path / "cell.toml")
48
+ _validate_cell_config(self._config)
49
+ return self._config
50
+
51
+ @property
52
+ def name(self) -> str:
53
+ return self._load_config()["cell"]["name"]
54
+
55
+ @property
56
+ def library(self):
57
+ return self._library
58
+
59
+ @property
60
+ def category(self):
61
+ if self._category is not None:
62
+ return self._category
63
+ return self._infer_category()
64
+
65
+ @property
66
+ def category_path(self) -> str | None:
67
+ category = self.category
68
+ return category.qualified_name if category is not None else None
69
+
70
+ @property
71
+ def qualified_name(self) -> str:
72
+ category_path = self.category_path
73
+ return f"{category_path}/{self.name}" if category_path else self.name
74
+
75
+ @property
76
+ def path(self) -> Path:
77
+ return self._path
78
+
79
+ def _infer_category(self):
80
+ library_path = getattr(self._library, "path", None)
81
+ if library_path is None:
82
+ return None
83
+ try:
84
+ relative_parent = self._path.parent.relative_to(Path(library_path))
85
+ except ValueError:
86
+ return None
87
+ if relative_parent == Path("."):
88
+ return None
89
+ category_path = Path(library_path) / relative_parent
90
+ if not (category_path / "category.toml").exists():
91
+ return None
92
+ from monata.category import Category
93
+
94
+ self._category = Category(category_path, self._library)
95
+ return self._category
96
+
97
+ def _views_config(self) -> dict[str, ViewConfig]:
98
+ return self._load_config().get("views", {})
99
+
100
+ def list_views(self) -> list:
101
+ return sorted(self._views_config().keys())
102
+
103
+ def __getitem__(self, view_type: str):
104
+ views = self._views_config()
105
+ if view_type not in views:
106
+ raise ViewNotFoundError(view_type, self.name)
107
+ cfg = views[view_type]
108
+ return create_registered_view(self, view_type, cfg)
109
+
110
+ def __contains__(self, view_type: str) -> bool:
111
+ return view_type in self._views_config()
112
+
113
+ def _is_generated_view(self, view_type: str) -> bool:
114
+ config = self._views_config().get(view_type)
115
+ if config is None:
116
+ return True
117
+ return bool(config.get("generated", True))
118
+
119
+ def _ensure_generated_view_writable(self, view_type: str, *, force: bool) -> None:
120
+ if not self._is_generated_view(view_type) and not force:
121
+ raise ViewAlreadyModifiedError(view_type, self.name)
122
+
123
+ def _set_view_config(self, view_type: str, view_config: ViewConfig) -> None:
124
+ config = self._load_config()
125
+ config.setdefault("views", {})[view_type] = view_config
126
+ write_cell_config(self._path / "cell.toml", config)
127
+ self._config = None
128
+
129
+ def _register_generated_view(self, view_type: str, *, entry: str) -> None:
130
+ self._set_view_config(
131
+ view_type,
132
+ create_registered_view_config(view_type, entry=entry, generated=True),
133
+ )
134
+
135
+ def write_generated_view(
136
+ self,
137
+ view_type: str,
138
+ *,
139
+ entry: str,
140
+ content: str,
141
+ force: bool = False,
142
+ ) -> Path:
143
+ """Write a generated view artifact and commit its view metadata."""
144
+
145
+ self._ensure_generated_view_writable(view_type, force=force)
146
+ view_path = self._path / entry
147
+ view_path.write_text(content)
148
+ self._register_generated_view(view_type, entry=entry)
149
+ return view_path
150
+
151
+ def create_view(self, view_type: str, **kwargs):
152
+ view_cfg = create_registered_view_config(view_type, **kwargs)
153
+ self._set_view_config(view_type, view_cfg)
154
+
155
+ return self[view_type]
156
+
157
+ def generate_symbol(self, force: bool = False) -> Path:
158
+ return self.generate_view("symbol", force=force)
159
+
160
+ def generate_netlist(
161
+ self,
162
+ force: bool = False,
163
+ format: str = "cir",
164
+ *,
165
+ projection: NetlistProjectionMode = "none",
166
+ registry: Any = None,
167
+ corner: Any = None,
168
+ ) -> Path:
169
+ return self.generate_view(
170
+ "netlist",
171
+ force=force,
172
+ format=format,
173
+ projection=projection,
174
+ registry=registry,
175
+ corner=corner,
176
+ )
177
+
178
+ def generate_view(self, view_type: str, **kwargs) -> Path:
179
+ return generate_registered_view(self, view_type, **kwargs)
@@ -0,0 +1,29 @@
1
+ """Reusable circuit construction helpers built on top of Monata netlists."""
2
+
3
+ from monata.circuits.cmos import (
4
+ TransistorParams,
5
+ add_inverter,
6
+ add_nand2,
7
+ add_nmos,
8
+ add_nor2,
9
+ add_pmos,
10
+ add_transmission_gate,
11
+ transistor_params,
12
+ )
13
+ from monata.circuits.source import (
14
+ build_source_subcircuit_instances,
15
+ source_subcircuit_ports,
16
+ )
17
+
18
+ __all__ = [
19
+ "TransistorParams",
20
+ "add_inverter",
21
+ "add_nand2",
22
+ "add_nmos",
23
+ "add_nor2",
24
+ "add_pmos",
25
+ "add_transmission_gate",
26
+ "build_source_subcircuit_instances",
27
+ "source_subcircuit_ports",
28
+ "transistor_params",
29
+ ]