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.
- monata/__init__.py +35 -0
- monata/_config.py +105 -0
- monata/_home.py +47 -0
- monata/_json.py +57 -0
- monata/_paths.py +73 -0
- monata/_types.py +8 -0
- monata/category.py +139 -0
- monata/cell.py +179 -0
- monata/circuits/__init__.py +29 -0
- monata/circuits/cmos.py +102 -0
- monata/circuits/source.py +70 -0
- monata/corner.py +355 -0
- monata/eda/__init__.py +35 -0
- monata/eda/kicad.py +526 -0
- monata/errors.py +33 -0
- monata/examples.py +79 -0
- monata/generation/__init__.py +6 -0
- monata/generation/netlist.py +72 -0
- monata/generation/symbol.py +26 -0
- monata/library.py +323 -0
- monata/measure/__init__.py +55 -0
- monata/measure/calculus.py +330 -0
- monata/measure/freq_domain.py +250 -0
- monata/measure/result.py +107 -0
- monata/measure/spec.py +385 -0
- monata/measure/statistics.py +91 -0
- monata/measure/summary.py +217 -0
- monata/measure/time_domain.py +362 -0
- monata/models/__init__.py +32 -0
- monata/models/artifacts.py +104 -0
- monata/models/cache.py +355 -0
- monata/models/compiler.py +239 -0
- monata/models/diagnostics.py +46 -0
- monata/models/flow.py +243 -0
- monata/models/manifest.py +438 -0
- monata/models/registry.py +369 -0
- monata/models/resolver.py +970 -0
- monata/netlist/__init__.py +45 -0
- monata/netlist/device_schema.py +394 -0
- monata/netlist/ir.py +1026 -0
- monata/netlist/mutation.py +173 -0
- monata/netlist/ngspice.py +221 -0
- monata/netlist/scope_api.py +1601 -0
- monata/netlist/scope_state.py +137 -0
- monata/netlist/topology.py +369 -0
- monata/optim/__init__.py +13 -0
- monata/optim/base.py +155 -0
- monata/optim/bayesian.py +187 -0
- monata/optim/circuit.py +166 -0
- monata/optim/nsga2.py +272 -0
- monata/parser/__init__.py +92 -0
- monata/parser/analysis_import.py +1009 -0
- monata/parser/commands.py +70 -0
- monata/parser/deck.py +348 -0
- monata/parser/errors.py +28 -0
- monata/parser/expression.py +457 -0
- monata/parser/import_plan.py +687 -0
- monata/parser/importer.py +636 -0
- monata/parser/source_subcircuit.py +53 -0
- monata/physics.py +309 -0
- monata/projection.py +238 -0
- monata/py.typed +0 -0
- monata/registry.py +59 -0
- monata/sim/__init__.py +5 -0
- monata/sim/_backend.py +28 -0
- monata/sim/_digital_bits.py +42 -0
- monata/sim/_frozen.py +124 -0
- monata/sim/_vector_identity.py +93 -0
- monata/sim/analysis_spec.py +310 -0
- monata/sim/artifacts.py +150 -0
- monata/sim/backends/__init__.py +41 -0
- monata/sim/backends/base.py +335 -0
- monata/sim/backends/ngspice.py +415 -0
- monata/sim/backends/ngspice_common.py +550 -0
- monata/sim/backends/ngspice_output.py +321 -0
- monata/sim/backends/ngspice_parse.py +377 -0
- monata/sim/backends/ngspice_plan.py +621 -0
- monata/sim/backends/ngspice_shared.py +344 -0
- monata/sim/backends/ngspice_shared_commands.py +65 -0
- monata/sim/backends/ngspice_shared_ffi.py +130 -0
- monata/sim/backends/ngspice_shared_session.py +621 -0
- monata/sim/backends/ngspice_stdout.py +237 -0
- monata/sim/cache.py +367 -0
- monata/sim/capabilities.py +191 -0
- monata/sim/core.py +77 -0
- monata/sim/corner.py +261 -0
- monata/sim/digital_circuits.py +199 -0
- monata/sim/digital_claims.py +310 -0
- monata/sim/digital_extract.py +402 -0
- monata/sim/digital_plan.py +505 -0
- monata/sim/digital_projection.py +111 -0
- monata/sim/digital_results.py +186 -0
- monata/sim/digital_spec.py +161 -0
- monata/sim/digital_table.py +512 -0
- monata/sim/digital_table_config.py +234 -0
- monata/sim/digital_tasks.py +225 -0
- monata/sim/digital_timing.py +109 -0
- monata/sim/executor.py +100 -0
- monata/sim/export.py +65 -0
- monata/sim/export_hdf5.py +339 -0
- monata/sim/export_payload.py +296 -0
- monata/sim/montecarlo.py +251 -0
- monata/sim/plot.py +156 -0
- monata/sim/rawfile.py +403 -0
- monata/sim/result_ops.py +140 -0
- monata/sim/results.py +782 -0
- monata/sim/session.py +382 -0
- monata/sim/sweep.py +272 -0
- monata/sim/task.py +152 -0
- monata/sim/vector_names.py +203 -0
- monata/sim/waveform.py +785 -0
- monata/spice_library.py +775 -0
- monata/techlib/__init__.py +5 -0
- monata/techlib/parse.py +187 -0
- monata/techlib/projection.py +168 -0
- monata/techlib/registry.py +501 -0
- monata/techlib/schema.py +130 -0
- monata/units.py +1251 -0
- monata/views/__init__.py +5 -0
- monata/views/base.py +155 -0
- monata/views/digital_truth_table.py +280 -0
- monata/views/netlist.py +15 -0
- monata/views/registry.py +332 -0
- monata/views/schematic.py +10 -0
- monata/views/simulation.py +312 -0
- monata/views/symbol.py +64 -0
- monata/views/testbench.py +16 -0
- monata/workspace/__init__.py +6 -0
- monata/workspace/experiment.py +93 -0
- monata/workspace/project.py +230 -0
- monata/workspace/result_store.py +177 -0
- monata-0.1.0.dist-info/METADATA +142 -0
- monata-0.1.0.dist-info/RECORD +135 -0
- monata-0.1.0.dist-info/WHEEL +4 -0
- 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
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
|
+
]
|