bdsim
bdsim copied to clipboard
Create SUM and PROD blocks via operator overload
Facilitates the creation of SUM and PROD blocks via overloading the operators:
- for SUM blocks:
- [x]
__add__, __radd__
(+
) - [x]
__sub__, __rsub__
(-
) - [x]
__neg__
(-
)
- [x]
- for PROD blocks:
- [x]
__mul__, __rmul__
(*
) - [x]
__div__, __rdiv__
(/
) - [x]
__matmul__, __rmatmul__
(@
)
- [x]
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.
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_EXPR
s and _PROD_EXPR
s 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.
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.
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.
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.
These are great ideas and included in bdsim
for a while now, but I cherry picked your code rather than accepting the PR.