Skip to content

Commit e22fb40

Browse files
authored
Compositional generators (#250)
Resolves #215. One of those rare PRs with more lines removed than added! Changes generators to be compositional: instead of a monolithic single `self.generator(fn, ...args...)`, this now invokes `self.generate()` (structurally like `self.contents()`) which can `self.get(...)` on any parameter. Parameters must be declared with `self.generator_param(...)` ahead of time (in `__init__` and `contents`), but this can be called multiple times. Refactors libraries to use mixins and have better separation of concerns. Major changes: - Parts table based selectors now have individual select functionality in each mixin class, e.g. the footprint class handles filtering by footprint spec, and the smd-standard-package class handles filtering by the minimum-smd-package - Microcontrollers (application circuits) no longer duplicate code (in each microcontroller class) for generating oscillators or stuffing additional pins onto SWD ports, these have been refactored into common mixins enabled by compositional generators. - Deduplicate microcontroller pin mapping / pin allocation code with the new `BaseIoControllerPinmapGenerator` base class. - (Syntactic) refactoring of libraries to use the new generators tyle This provides no functionality changes (aside from the generator API, though a deprecation is kept), this is an implementation change only. Future features will build on this base infrastructure. Minor changes: - Capacitors: unifies the single and multiple-parallel code - The microcontroller refactor changes the connection and component ordering, which changes the netlists. However, the netlists should still be structurally the same (isomorphic). - Add standard pinning to the (abstract) LED base class - Refactor FT232 to have port arrays on ACBUS and ADBUS - Add types to requirements.txt --- Notes: - Another generator API could be defining the generator params as objects using `param = self.GeneratorParam(...wrapped...)` and supporting `param.get()`, akin to `self.ArgParameter(...)`. This was actually attempted first, but in practice this creates a duplicate object with the underlying parameter so it's not clear what should be used where. However, it is a bit more type-safe (since current we can `self.get(...)` anything, whereas if you have a GeneratorParam object, `.get()` is guaranted as long as it's within the generate phase). - This causes a 10-15% performance penalty in the unit test suite. Likely, the parts table selectors are a lot less efficient, because of `super()` call overhead, and possibly other additional work done because while the new structure enables code deduplication, it constrains optimizations. In particular, the unified single and multiple-parallel capacitors does more work in the single case now since parallel caps are being computed for all cases, across the entire parts table. - Some parts are have potentially bad performance - Resistor arrays repeat redundant calculations per-row. - This supports internal-facing mixins (how blocks can share implementations) per #104, while external mixins (eg, how to specify "I want an abstract voltage regulator with a EN pin") would be a future feature. - Closes #166, since this uses `self.connect(...)` to create a node in a way where it can't have type information. In the bigger picture, it probably makes sense to separate the capabilities of a port and the capabilities of a connection.
1 parent 6288568 commit e22fb40

File tree

93 files changed

+2036
-2351
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+2036
-2351
lines changed

edg_core/Array.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ def __getitem__(self, item: str) -> VectorType:
125125
assert self._elts is not None, "no elts defined"
126126
return self._elts[item]
127127

128+
def items(self) -> ItemsView[str, VectorType]:
129+
assert self._elts is not None, "no elts defined"
130+
return self._elts.items()
131+
128132
# unlike most other LibraryElement types, the names are stored in _elts and _allocates
129133
def _name_of_child(self, subelt: Any) -> str:
130134
from .HierarchyBlock import Block

edg_core/Generator.py

Lines changed: 61 additions & 158 deletions
Large diffs are not rendered by default.

edg_core/HdlUserExceptions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@ class BlockDefinitionError(EdslUserError):
4747
def __init__(self, block, exc: str, resolution: str = ''):
4848
super().__init__(f"invalid block definition for {type(block)}: {exc}", resolution)
4949

50+
5051
class ChainError(BlockDefinitionError):
5152
"""Base error for bad elements in a chain connect"""

edg_core/test_connect_array.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ class TestBlockSinkElasticArray(GeneratorBlock):
1717
def __init__(self) -> None:
1818
super().__init__()
1919
self.sinks = self.Port(Vector(TestPortSink()))
20-
self.generator(self.generate, self.sinks.requested())
20+
self.generator_param(self.sinks.requested())
2121

22-
def generate(self, requests: List[str]):
23-
for request in requests:
22+
def generate(self):
23+
super().generate()
24+
for request in self.get(self.sinks.requested()):
2425
self.sinks.append_elt(TestPortSink(), request)
2526

2627

edg_core/test_generator.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ def __init__(self) -> None:
1515
super().__init__()
1616
# Because this doesn't have dependency parameters, this is the top-level design
1717
self.float_param = self.Parameter(FloatExpr())
18-
self.generator(self.float_gen)
1918

20-
def float_gen(self) -> None:
19+
def generate(self) -> None:
2120
self.assign(self.float_param, 2.0)
2221

2322

@@ -32,10 +31,11 @@ class GeneratorDependency(GeneratorBlock):
3231
def __init__(self, float_preset: FloatLike) -> None:
3332
super().__init__()
3433
self.float_param = self.Parameter(FloatExpr())
35-
self.generator(self.float_gen, float_preset)
34+
self.float_preset = self.ArgParameter(float_preset)
35+
self.generator_param(self.float_preset)
3636

37-
def float_gen(self, float_preset: float) -> None:
38-
self.assign(self.float_param, float_preset * 2)
37+
def generate(self) -> None:
38+
self.assign(self.float_param, self.get(self.float_preset) * 2)
3939

4040

4141
class TestGeneratorMultiParameter(Block):
@@ -50,11 +50,13 @@ def __init__(self, float_preset1: FloatLike, float_preset2: FloatLike) -> None:
5050
super().__init__()
5151
self.float_param1 = self.Parameter(FloatExpr())
5252
self.float_param2 = self.Parameter(FloatExpr())
53-
self.generator(self.float_gen, float_preset1, float_preset2)
53+
self.float_preset1 = self.ArgParameter(float_preset1)
54+
self.float_preset2 = self.ArgParameter(float_preset2)
55+
self.generator_param(self.float_preset1, self.float_preset2)
5456

55-
def float_gen(self, float_preset1: float, float_preset2: float) -> None:
56-
self.assign1 = self.assign(self.float_param1, float_preset1 * 3)
57-
self.assign2 = self.assign(self.float_param2, float_preset2 + 7)
57+
def generate(self) -> None:
58+
self.assign1 = self.assign(self.float_param1, self.get(self.float_preset1) * 3)
59+
self.assign2 = self.assign(self.float_param2, self.get(self.float_preset2) + 7)
5860

5961

6062
class TestGenerator(unittest.TestCase):
@@ -118,11 +120,12 @@ class GeneratorIsConnected(GeneratorBlock):
118120
def __init__(self) -> None:
119121
super().__init__()
120122
self.port = self.Port(TestPortSource(2.0), optional=True)
121-
self.generator(self.generate_assign, self.port.is_connected())
123+
124+
self.generator_param(self.port.is_connected())
122125
self.connected = self.Parameter(BoolExpr())
123126

124-
def generate_assign(self, connected: bool) -> None:
125-
if connected:
127+
def generate(self) -> None:
128+
if self.get(self.port.is_connected()):
126129
self.assign(self.connected, True)
127130
else:
128131
self.assign(self.connected, False)
@@ -146,7 +149,6 @@ class GeneratorInnerConnect(GeneratorBlock):
146149
def __init__(self) -> None:
147150
super().__init__()
148151
self.port = self.Port(TestPortSource(), optional=True)
149-
self.generator(self.generate)
150152

151153
def generate(self) -> None:
152154
self.inner = self.Block(TestBlockSource(4.5))
@@ -194,9 +196,8 @@ def __init__(self) -> None:
194196
class GeneratorFailure(GeneratorBlock):
195197
def __init__(self) -> None:
196198
super().__init__()
197-
self.generator(self.errorfn)
198199

199-
def errorfn(self) -> None:
200+
def generate(self) -> None:
200201
def helperfn() -> None:
201202
raise TestGeneratorException("test text")
202203
helperfn()

edg_core/test_generator_error.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,29 @@
11
import unittest
22

33
from . import *
4-
from .ScalaCompilerInterface import ScalaCompiler
4+
from .HdlUserExceptions import BlockDefinitionError
55

66

77
class BadGeneratorTestCase(unittest.TestCase):
88
# These are internal classes to avoid this error case being auto-discovered in a library
99

1010
class InvalidMissingGeneratorBlock(GeneratorBlock):
11-
def __init__(self) -> None:
12-
super().__init__()
13-
# this is missing a self.generator statement
11+
"""This doesn't implement generator()"""
1412

1513
def test_missing_generator(self) -> None:
16-
with self.assertRaises(AssertionError):
14+
with self.assertRaises(BlockDefinitionError):
1715
self.InvalidMissingGeneratorBlock()._elaborated_def_to_proto()
1816

1917

20-
class InvalidMultiGeneratorBlock(GeneratorBlock):
21-
def __init__(self) -> None:
22-
super().__init__()
23-
self.generator(self.dummy_gen)
24-
self.generator(self.dummy_gen)
25-
26-
def dummy_gen(self) -> None:
27-
pass
28-
29-
def test_multi_generator(self) -> None:
30-
with self.assertRaises(AssertionError):
31-
self.InvalidMultiGeneratorBlock()._elaborated_def_to_proto()
32-
33-
3418
class InvalidNonArgGeneratorBlock(GeneratorBlock):
3519
def __init__(self) -> None:
3620
super().__init__()
3721
self.param = self.Parameter(FloatExpr())
38-
self.generator(self.dummy_gen, self.param)
22+
self.generator_param(self.param)
3923

40-
def dummy_gen(self, x: float) -> None:
41-
pass
24+
def generate(self) -> None:
25+
super().generate()
4226

4327
def test_non_arg_generator(self) -> None:
44-
with self.assertRaises(AssertionError):
28+
with self.assertRaises(BlockDefinitionError):
4529
self.InvalidNonArgGeneratorBlock()._elaborated_def_to_proto()

edg_core/test_generator_portvector.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ class GeneratorInnerBlock(GeneratorBlock):
1111
def __init__(self) -> None:
1212
super().__init__()
1313
self.ports = self.Port(Vector(TestPortSink()))
14-
self.generator(self.generate, self.ports.requested())
14+
self.generator_param(self.ports.requested())
1515

16-
def generate(self, elements: List[str]) -> None:
17-
assert elements == ['0', 'named', '1'], f"bad elements {elements}"
16+
def generate(self) -> None:
17+
assert self.get(self.ports.requested()) == ['0', 'named', '1']
1818
self.ports.append_elt(TestPortSink((-1, 1)))
1919
self.ports.append_elt(TestPortSink((-5, 5)), 'named')
2020
self.ports.append_elt(TestPortSink((-2, 2)))
@@ -48,29 +48,26 @@ def test_initializer(self):
4848
self.assertEqual(pb.constraints[3].value, edgir.AssignLit(['ports', '1', 'range_param'], Range(-2, 2)))
4949

5050

51-
class GeneratorInnerBlockInvalid(GeneratorBlock):
51+
class InnerBlockInvalid(Block):
5252
def __init__(self) -> None:
5353
super().__init__()
5454
self.ports = self.Port(Vector(TestPortSink()))
55-
self.generator(self.generate, self.ports.requested())
56-
57-
def generate(self, elements: List[str]) -> None:
5855
self.ports.append_elt(TestPortSink(), 'haha')
5956

6057

61-
class TestGeneratorElementsInvalid(Block):
58+
class TestElementsInvalid(Block):
6259
def __init__(self) -> None:
6360
super().__init__()
64-
self.block = self.Block(GeneratorInnerBlockInvalid())
61+
self.block = self.Block(InnerBlockInvalid())
6562

6663
self.source0 = self.Block(TestBlockSource(1.0))
6764
self.connect(self.source0.port, self.block.ports.request('nope'))
6865

6966

70-
class TestGeneratorPortVectorInvalid(unittest.TestCase):
67+
class TestPortVectorInvalid(unittest.TestCase):
7168
def test_generator_error(self):
7269
with self.assertRaises(CompilerCheckError):
73-
ScalaCompiler.compile(TestGeneratorElementsInvalid)
70+
ScalaCompiler.compile(TestElementsInvalid)
7471

7572

7673
class GeneratorWrapperBlock(Block):
@@ -119,10 +116,11 @@ class GeneratorArrayParam(GeneratorBlock):
119116
def __init__(self, param: ArrayRangeLike) -> None:
120117
super().__init__()
121118
self.ports = self.Port(Vector(TestPortSink()))
122-
self.generator(self.generate, param)
119+
self.param = self.ArgParameter(param)
120+
self.generator_param(self.param)
123121

124-
def generate(self, elements: List[Range]) -> None:
125-
for elt in elements:
122+
def generate(self) -> None:
123+
for elt in self.get(self.param):
126124
created_port = self.ports.append_elt(TestPortSink(elt)) # any port
127125
self.require(created_port.link().sinks_range == Range(-2, 1))
128126

electronics_abstract_parts/AbstractAnalogSwitch.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,15 @@ class AnalogSwitchTree(AnalogSwitch, GeneratorBlock):
4040
@init_in_parent
4141
def __init__(self, switch_size: IntLike = 0):
4242
super().__init__()
43-
self.generator(self.generate, self.inputs.requested(), switch_size)
43+
self.switch_size = self.ArgParameter(switch_size)
44+
self.generator_param(self.switch_size, self.inputs.requested())
4445

45-
def generate(self, elts: List[str], switch_size: int):
46+
def generate(self):
4647
import math
48+
super().generate()
4749

50+
switch_size = self.get(self.switch_size)
51+
elts = self.get(self.inputs.requested())
4852
assert switch_size > 1, f"switch size {switch_size} must be greater than 1"
4953
assert len(elts) > 1, "passthrough AnalogSwitchTree not (yet?) supported"
5054
self.sw = ElementDict[AnalogSwitch]()
@@ -121,11 +125,12 @@ def __init__(self) -> None:
121125
impedance=self.device.analog_on_resistance + self.inputs.hull(lambda x: x.link().source_impedance)
122126
)))
123127

124-
self.generator(self.generate, self.inputs.requested())
128+
self.generator_param(self.inputs.requested())
125129

126-
def generate(self, elts: List[str]):
130+
def generate(self):
131+
super().generate()
127132
self.inputs.defined()
128-
for elt in elts:
133+
for elt in self.get(self.inputs.requested()):
129134
self.connect(
130135
self.inputs.append_elt(AnalogSink().empty(), elt),
131136
self.device.inputs.request(elt).adapt_to(AnalogSink(
@@ -161,11 +166,12 @@ def __init__(self) -> None:
161166
impedance=self.device.analog_on_resistance + self.outputs.hull(lambda x: x.link().sink_impedance)
162167
)))
163168

164-
self.generator(self.generate, self.outputs.requested())
169+
self.generator_param(self.outputs.requested())
165170

166-
def generate(self, elts: List[str]):
171+
def generate(self):
172+
super().generate()
167173
self.outputs.defined()
168-
for elt in elts:
174+
for elt in self.get(self.outputs.requested()):
169175
self.connect(
170176
self.outputs.append_elt(AnalogSource().empty(), elt),
171177
self.device.inputs.request(elt).adapt_to(AnalogSource(

0 commit comments

Comments
 (0)