circuit-static-description 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.
@@ -0,0 +1,10 @@
1
+ """Circuit static description Python package."""
2
+
3
+ from .circuit import Circuit, CircuitError, CircuitFormatError
4
+
5
+ __all__ = ["Circuit", "CircuitError", "CircuitFormatError"]
6
+
7
+
8
+ def main() -> None:
9
+ """Simple package entry point for basic verification."""
10
+ print("circuit_static_description package is available")
@@ -0,0 +1,212 @@
1
+ """Circuit load/save/evaluate implementation for boolean gate circuits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, List, Sequence, Tuple
8
+
9
+
10
+ class CircuitError(Exception):
11
+ """Base exception raised by circuit_static_description."""
12
+
13
+
14
+ class CircuitFormatError(CircuitError):
15
+ """Raised when a circuit description file cannot be parsed."""
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class _ExprNode:
20
+ op: str
21
+ args: Tuple["_ExprNode", ...] = ()
22
+ input_index: int | None = None
23
+
24
+
25
+ class Circuit:
26
+ """A boolean logic circuit with only input count, output count and output expressions."""
27
+
28
+ SUPPORTED_OPS = {
29
+ "AND": 2,
30
+ "OR": 2,
31
+ "NOT": 1,
32
+ "XOR": 2,
33
+ "NAND": 2,
34
+ "NOR": 2,
35
+ }
36
+
37
+ def __init__(self, input_count: int, output_count: int, outputs: List[str] | None = None) -> None:
38
+ if input_count < 1:
39
+ raise CircuitError("input_count must be at least 1")
40
+ if output_count < 1:
41
+ raise CircuitError("output_count must be at least 1")
42
+ self.input_count = input_count
43
+ self.output_count = output_count
44
+ self.outputs = outputs or ["" for _ in range(output_count)]
45
+ if len(self.outputs) != self.output_count:
46
+ raise CircuitError("outputs length must match output_count")
47
+
48
+ def save(self, path: str | Path) -> None:
49
+ path = Path(path)
50
+ path.write_text(self.to_text(), encoding="utf-8")
51
+
52
+ def to_text(self) -> str:
53
+ lines: List[str] = [f"INPUTS {self.input_count}", f"OUTPUTS {self.output_count}"]
54
+ for index, expression in enumerate(self.outputs):
55
+ lines.append(f"OUT{index} = {expression}")
56
+ return "\n".join(lines) + "\n"
57
+
58
+ @classmethod
59
+ def load(cls, path: str | Path) -> "Circuit":
60
+ path = Path(path)
61
+ return cls.from_text(path.read_text(encoding="utf-8"))
62
+
63
+ @classmethod
64
+ def from_text(cls, text: str) -> "Circuit":
65
+ lines = [line.split("#", 1)[0].strip() for line in text.splitlines()]
66
+ lines = [line for line in lines if line]
67
+ if len(lines) < 2:
68
+ raise CircuitFormatError("Circuit description must include INPUTS and OUTPUTS")
69
+
70
+ def parse_header(prefix: str, line: str) -> int:
71
+ if not line.upper().startswith(prefix):
72
+ raise CircuitFormatError(f"Expected '{prefix}' header, got: {line}")
73
+ parts = line.split(None, 1)
74
+ if len(parts) != 2 or not parts[1].isdigit():
75
+ raise CircuitFormatError(f"Invalid {prefix} header: {line}")
76
+ return int(parts[1])
77
+
78
+ input_count = parse_header("INPUTS", lines[0])
79
+ output_count = parse_header("OUTPUTS", lines[1])
80
+ if len(lines) != output_count + 2:
81
+ raise CircuitFormatError(
82
+ f"Expected {output_count} output lines after headers, got {len(lines) - 2}"
83
+ )
84
+
85
+ outputs: List[str] = []
86
+ for index, line in enumerate(lines[2:]):
87
+ if "=" not in line:
88
+ raise CircuitFormatError(f"Missing '=' on output line {index}: {line}")
89
+ name, expr = [part.strip() for part in line.split("=", 1)]
90
+ expected_name = f"OUT{index}"
91
+ if name.upper() != expected_name:
92
+ raise CircuitFormatError(f"Expected output name '{expected_name}', got '{name}'")
93
+ if not expr:
94
+ raise CircuitFormatError(f"Empty expression for {expected_name}")
95
+ outputs.append(expr)
96
+
97
+ circuit = cls(input_count=input_count, output_count=output_count, outputs=outputs)
98
+ return circuit
99
+
100
+ def evaluate(self, inputs: Sequence[Any]) -> List[int]:
101
+ if len(inputs) != self.input_count:
102
+ raise CircuitError(
103
+ f"Expected {self.input_count} input values, got {len(inputs)}"
104
+ )
105
+ parsed_outputs = [self._parse_expression(expression) for expression in self.outputs]
106
+ return [int(bool(self._evaluate_node(node, inputs))) for node in parsed_outputs]
107
+
108
+ def _parse_expression(self, text: str) -> _ExprNode:
109
+ text = text.strip()
110
+ if not text:
111
+ raise CircuitFormatError("Expression cannot be empty")
112
+ parser = _ExpressionParser(text)
113
+ node = parser.parse()
114
+ if not parser.is_done():
115
+ raise CircuitFormatError(f"Unexpected text after expression: {parser.remaining_text()}")
116
+ return node
117
+
118
+ def _evaluate_node(self, node: _ExprNode, inputs: Sequence[Any]) -> bool:
119
+ if node.op == "INPUT":
120
+ assert node.input_index is not None
121
+ if node.input_index < 0 or node.input_index >= len(inputs):
122
+ raise CircuitError(f"Input reference I{node.input_index} is out of range")
123
+ return bool(inputs[node.input_index])
124
+ values = [self._evaluate_node(child, inputs) for child in node.args]
125
+ if node.op == "AND":
126
+ return values[0] and values[1]
127
+ if node.op == "OR":
128
+ return values[0] or values[1]
129
+ if node.op == "NOT":
130
+ return not values[0]
131
+ if node.op == "XOR":
132
+ return values[0] ^ values[1]
133
+ if node.op == "NAND":
134
+ return not (values[0] and values[1])
135
+ if node.op == "NOR":
136
+ return not (values[0] or values[1])
137
+ raise CircuitError(f"Unsupported operator during evaluation: {node.op}")
138
+
139
+
140
+ class _ExpressionParser:
141
+ def __init__(self, text: str) -> None:
142
+ self.text = text
143
+ self.pos = 0
144
+
145
+ def parse(self) -> _ExprNode:
146
+ node = self._parse_term()
147
+ return node
148
+
149
+ def is_done(self) -> bool:
150
+ self._skip_whitespace()
151
+ return self.pos >= len(self.text)
152
+
153
+ def remaining_text(self) -> str:
154
+ return self.text[self.pos :].strip()
155
+
156
+ def _parse_term(self) -> _ExprNode:
157
+ self._skip_whitespace()
158
+ if self._peek().isalpha():
159
+ token = self._parse_identifier().upper()
160
+ if token.startswith("I") and token[1:].isdigit():
161
+ index = int(token[1:])
162
+ if index < 0:
163
+ raise CircuitFormatError(f"Invalid input reference: {token}")
164
+ return _ExprNode(op="INPUT", input_index=index)
165
+ if token not in Circuit.SUPPORTED_OPS:
166
+ raise CircuitFormatError(f"Unsupported operator: {token}")
167
+ self._skip_whitespace()
168
+ if self._peek() != "(":
169
+ raise CircuitFormatError(f"Operator '{token}' requires parentheses")
170
+ self._consume("(")
171
+ args = self._parse_arguments(token)
172
+ self._consume(")")
173
+ expected = Circuit.SUPPORTED_OPS[token]
174
+ if len(args) != expected:
175
+ raise CircuitFormatError(
176
+ f"Operator '{token}' expects {expected} arguments, got {len(args)}"
177
+ )
178
+ return _ExprNode(op=token, args=tuple(args))
179
+ raise CircuitFormatError("Expression must begin with an input reference or operator")
180
+
181
+ def _parse_arguments(self, operator: str) -> List[_ExprNode]:
182
+ arguments: List[_ExprNode] = []
183
+ while True:
184
+ self._skip_whitespace()
185
+ arguments.append(self._parse_term())
186
+ self._skip_whitespace()
187
+ if self._peek() == ")":
188
+ break
189
+ self._consume(",")
190
+ return arguments
191
+
192
+ def _parse_identifier(self) -> str:
193
+ start = self.pos
194
+ while self.pos < len(self.text) and self.text[self.pos].isalnum():
195
+ self.pos += 1
196
+ return self.text[start : self.pos]
197
+
198
+ def _skip_whitespace(self) -> None:
199
+ while self.pos < len(self.text) and self.text[self.pos].isspace():
200
+ self.pos += 1
201
+
202
+ def _peek(self) -> str:
203
+ self._skip_whitespace()
204
+ if self.pos >= len(self.text):
205
+ return ""
206
+ return self.text[self.pos]
207
+
208
+ def _consume(self, expected: str) -> None:
209
+ self._skip_whitespace()
210
+ if self.pos >= len(self.text) or self.text[self.pos] != expected:
211
+ raise CircuitFormatError(f"Expected '{expected}' at position {self.pos}")
212
+ self.pos += 1
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: circuit-static-description
3
+ Version: 0.1.0
4
+ Summary: A boolean gate circuit description package for saving, loading, and evaluating logic circuits.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: circuit,boolean,logic
8
+ Author: GGN_2015
9
+ Author-email: neko@jlulug.org
10
+ Requires-Python: >=3.11,<4.0
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Description-Content-Type: text/markdown
19
+
20
+ # circuit-static-description
21
+ A text-based boolean gate circuit description format that is easy to save and port.
22
+
23
+ ## Installation
24
+
25
+ Install from PyPI:
26
+
27
+ ```bash
28
+ pip install circuit-static-description
29
+ ```
30
+
31
+ ## Python package usage
32
+
33
+ This project provides a Python package named `circuit_static_description`. The package supports saving circuits, loading circuits, and evaluating outputs.
34
+
35
+ ### Importing
36
+
37
+ ```python
38
+ from circuit_static_description import Circuit
39
+ ```
40
+
41
+ ### Circuit description format
42
+
43
+ A circuit description contains only the number of inputs, the number of outputs, and the expression for each output. There are no intermediate variable names.
44
+
45
+ - Input references use `I0`, `I1`, `I2`, etc.
46
+ - Output lines use fixed names `OUT0`, `OUT1`, etc.
47
+ - Supported logic operators: `AND`, `OR`, `NOT`, `XOR`, `NAND`, `NOR`.
48
+
49
+ Example:
50
+
51
+ ```text
52
+ INPUTS 3
53
+ OUTPUTS 2
54
+ OUT0 = AND(I0, I1)
55
+ OUT1 = NOR(I2, XOR(I0, I1))
56
+ ```
57
+
58
+ ### Saving a circuit
59
+
60
+ ```python
61
+ from circuit_static_description import Circuit
62
+
63
+ circuit = Circuit(
64
+ input_count=3,
65
+ output_count=2,
66
+ outputs=[
67
+ "AND(I0, I1)",
68
+ "NOR(I2, XOR(I0, I1))",
69
+ ],
70
+ )
71
+
72
+ circuit.save("example.circuit")
73
+ ```
74
+
75
+ ### Loading a circuit
76
+
77
+ ```python
78
+ from circuit_static_description import Circuit
79
+
80
+ circuit = Circuit.load("example.circuit")
81
+ ```
82
+
83
+ ### Evaluating a circuit
84
+
85
+ ```python
86
+ result = circuit.evaluate([1, 0, 1])
87
+ print(result)
88
+ # Example output: [0, 0]
89
+ ```
90
+
91
+ ### Notes
92
+
93
+ - `evaluate` accepts a list of input values in input order.
94
+ - The output is returned as a list of `0` or `1` values.
95
+ - Expressions cannot use custom variable names; they must use `I*` input references and supported logic operators.
@@ -0,0 +1,6 @@
1
+ circuit_static_description/__init__.py,sha256=6ALgrX2IErZnQk0inRs2hoTXDi8GN6Fu1VjJwcqjSO4,328
2
+ circuit_static_description/circuit.py,sha256=SbdLzOylkLvmNsqi_VY6gLIxVAKa7mDwMr9rC0TRTnA,8382
3
+ circuit_static_description-0.1.0.dist-info/licenses/LICENSE,sha256=gmkEFqkF3KJjRnhXGoLSfX4xXtfPHT1ghq3YgCF7B3w,1086
4
+ circuit_static_description-0.1.0.dist-info/METADATA,sha256=8pwVimPn2Dgku78OHzCzg_WD-qXjiNJHENbZ8wy93LY,2362
5
+ circuit_static_description-0.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
6
+ circuit_static_description-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GGN_2015
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.