BerkeleyHCI/PolymorphicBlocks

Generator composition

Closed this issue · 0 comments

While single-shot generators solved problems with generators arbitrarily appending data to a block (and in a way that doesn't compose easily, like making redundant links that need to be merged), maybe it's a bit too restrictive and limits composability of generators.

Example use cases

Part table

It may make sense to have these base classes for a table-based resistor part:

  • AbstractResistor - defines resistance (spec parameter)
  • PartTablePart - base class that selects parts from a part table
    • Defines abstract part_matches(candidate_part) -> bool, that defines whether some candidate part matches the current spec
    • Defines abstract get_table() that returns the table
    • Defines abstract selected_part(part) -> None that is called on the selected part table part
  • PartTableResistor - extends AbstractResistor and PartTablePart
    • Implements part_matches(...) based on the resistance spec. Generator dependency on resistance.
  • SmtStandardPart - base class that defines parts that can generate into 0402/0603/0805/... series and provides a minimum_package parameter (without an implementation)
  • MyPartTableResistor - extends PartTableResistor and SmtStandardPart
    • But this needs to change the part_matches behavior and now also depends on minimum_package - which changes the generator signature previously defined in PartTableResistor.
    • Implements get_table() with the relevant table and selected_part(...) to generate the part

Somewhat abstractly, this use case is composing base classes which define generator snippets that need different pieces of data. So the current structure of a single generator function makes it monolithic and ends up preventing modularity and re-use.

Mixins: microcontrollers

In some subcircuits, parts may be re-usable. For example, a microcontroller block might automatically generate the programming port, for example HasSwdPort might be a mixin to a Microcontroller. This would define a SWD port externally, and if it's not connected, generate a programming header internally. Though note, there would need to be an internal node for the interface, not sure how that would be implemented yet

But if the top level microcontroller is also a generator, then this would need two generators, or HasSwdPort would have a utility function called by a top-level monolithic generator and components. This structure isn't unworkable, but all parameters must be plumbed through the top generator, and that function can get unwieldly quickly.

Ideas

Declaring generator parameters

Maybe it makes sense to replace ArgParam with GeneratorParam, and this is used to register which parameters a generator depends on. Crucially, this could be composed additively (superclasses can declare GeneratorParams, we don't need all of them declared at once like the current self.generator(fn, *params)).

The generator fn would then be replaced with generator(), much like contents(). It takes no arguments, but within this function, GeneratorParam values can be accessed with .getValue, that returns the corresponding solved Python value. Overall this makes generators much more consistent stylistically with non-generators. GeneratorParam can also define a default value that doesn't need to be tied to init default args.

This might not even need a compiler core change, it could be done in the frontend only since the core generator semantics don't change (call some Python function once inputs are available, and that fills in a Block's implementation).

Generator directed assignments

Full generators may be overkill for applications where the circuit doesn't need to be generated - these are cases where only a value needs to be generated because the logic is outside the scope of the mini expression language, and we need to fall back to Python. An option might be to have an expression node that is a Python expression, while the rest of the Block is defined as normal.

Potential benefits:

  • no longer limited to generator semantics where it can't depend on values of inner blocks - for example, it can do arbitrary calculations based on post-generate values of child blocks
    • currently, that behavior could be emulated by putting it into another child block, which runs its generate after the value is available - this is a bit of a mess
    • this is also the reason behind a separate BuckConverterPowerPath, since the output voltage may be needed to calculate the output current which is needed to generate, but the output voltage is defined by an internal block
  • holes in the design graph are tighter - the whole thing doesn't need to be a generator just become one of the calculations needs a specialty function

Potential issues:

  • currently, memoizing generator outputs probably provides a significant speedup (so every single 0.1uF cap doesn't need to re-check the parts table), something similar would be needed at this level.
  • how to deal with multiple-input, multiple-output functions syntactically?
  • yet another concept to deal with and learn for library builders

Other problems

  • Currently, the only support for default values is through the init param system which is pretty hacky and requires plumbing all params to init, even ones that aren't meant to be visible to block instantiators. But this is needed since generator contents aren't defined pre-generator, which includes internal assigns.
    • Alternatives: a more general infrastructure to improve default parameters? Maybe ArgParam can take (init param, optional default)? Maybe defaults should be assigned by infrastructure instead of in the containing block (eg, containing block can define param values, but if there is none assigned to an arg-param specifically, the compiler inserts the default fetched from the internal block's definition using the current defaults-for-subclasses infrastructure)?