bdsim icon indicating copy to clipboard operation
bdsim copied to clipboard

Create SUM and PROD blocks via operator overload

Open CallumJHays opened this issue 3 years ago • 4 comments

Facilitates the creation of SUM and PROD blocks via overloading the operators:

  • for SUM blocks:
    • [x] __add__, __radd__ (+)
    • [x] __sub__, __rsub__ (-)
    • [x] __neg__ (-)
  • for PROD blocks:
    • [x] __mul__, __rmul__ (*)
    • [x] __div__, __rdiv__ (/)
    • [x] __matmul__, __rmatmul__ (@)

If an operand of numeric type (ndarray, float, int, complex) is used, is automatically wrapped in a bd.CONSTANT. This enables expressions that would typically be extremely verbose in bdsim to be expressed elegantly:

sine = bd.WAVEFORM('sine')
square = bd.WAVEFORM('square')
average = (sine + square) / 2

Instead of:

sine = bd.WAVEFORM('sine')
square = bd.WAVEFORM('square')
sum = bd.SUM('++', sine, square)
n = bd.CONSTANT(2)
average = bd.PROD('*/', sum, n)

Potential Future Work:

  • Logical block support (&, ^, |, <, <=, >, >=, ==, !=, ~)
  • Pow (**), abs(), Mod (%), divmod()
  • Rounding functions (round(), ceil(), floor(), //)

Notes:

Previously block1[0] * block2[0] was equivalent to bd.connect(block1[0], block2[0]). Now that * means multiplication, implicit wiring has switched to the >> operator. Therefore, block1[0] >> block2[0] is now equivalent to bd.connect(block1[0], block2[0]). I believe this is more intuitive.

CallumJHays avatar May 16 '21 11:05 CallumJHays

Update: I've hit a snag using this method. Consider the following code:

sine = bd.WAVEFORM('sine')
square = bd.WAVEFORM('square')
combined = sine + square
offset_1 = combined + 1
offset_2 = combined - 1

Due to the proposed 'operation chaining' (altering existing SUM and PROD blocks rather than wrapping in new ones), the code will result in incorrect behaviour as combined, offset_1 and offset_2 will all point to the same block object, and will all have the output value of sine + square + 1 - 1.

A solution I can think of is to use private, intermediate and nested _SUM_EXPR and _PROD_EXPR blocks that do not get added to the bd.blocklist until they are connected() to another block in user code, at which point the nested _SUM_EXPRs and _PROD_EXPRs will be flattened and consolidated into SUM and PROD blocks.

Note that the auto-naming of these intermediate blocks may seem out-of-order. For example:

combined = sine + square
offset_1 = bd.SUM('++', combined, bd.CONSTANT(1))
offset_2 = combined - 1
bd.PRINT(combined, offset_1, offset_2)

will print:

print.0(t=...):
    sum.1 = ...
    sum.0 = ...
    sum.2 = ...

Whereas one would expect them to be the order in which they were defined, ie:

print.0(t=...):
    sum.0 = ...
    sum.1 = ...
    sum.2 = ...

Note that the actual output of bd.PRINT is quite different but you get the idea.

A solution to this could be; These intermediate blocks could track their definition order wrt. blocks in the blocklist so that during the bd.compile() stage, the consolidated blocks will be auto-named appropriately. This will also require block auto-naming to occur at bd.compile(), which could also be confusing. Currently, auto-naming occurs at definition time.

I will continue working on the functional correctness, but leave the auto-naming woes for another PR.

CallumJHays avatar May 17 '21 07:05 CallumJHays

The changes mentioned in my previous comment have been implemented and according to the tests I've written everything appears to be functioning correctly and should be ready to merge into master.

PS: I've added another feature. When a 'prod' block would be produced with exactly two inputs, where one of them are numerical constants, they are instead produced as a 'gain' block. This reduces the amount of CONSTANT blocks produced to the bare minimum and leads to a more natural block-diagram.

CallumJHays avatar May 24 '21 13:05 CallumJHays

I think this will lead to a major lift in productivity in creating diagrams, it's a really cool idea.

I haven't looked at the code yet, but I don't quite know why you have a snag. Consider just this bit

sine = bd.WAVEFORM('sine')
square = bd.WAVEFORM('square')
combined = sine + square

then __add__ could just return a brand new adder bd.sum('++', op1, op2) which already supports inputs as arguments, ie. they get wired in. Something like

def __add__(left, right):
  return bd.sum('++', left, right)

Then combined would be a reference to this new summing junction. It would need a unique name but some systematic convention would be fine.

a+b+c+d would be a tree of adders which is not ideal but can't do any better at this operator level, it would need post processing to cleanup and probs not worth it.

Same approach should work for products as well.

petercorke avatar May 30 '21 22:05 petercorke

More thoughts.

Key ideas to include/take forward:

  • auto generation of SUM and PROD blocks by + and * operators
  • replace existing * operator with >>, makes direction of composition much clearer, was confusing before
  • turning PROD with CONSTANT to GAIN block
  • supporting more operators such as relational and logical. Should these be bitwise or boolean?

In the case of

A = B[1] + C[2]

we are passed Plug object instances not blocks. Shouldn't really be an issue since connect takes them or Block instances.

petercorke avatar May 30 '21 23:05 petercorke

These are great ideas and included in bdsim for a while now, but I cherry picked your code rather than accepting the PR.

petercorke avatar Jan 22 '23 22:01 petercorke