From 1bfd2a18495d2d29ac2ee23ba5fb892d6bf8321d Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Wed, 20 May 2026 00:52:32 -0700 Subject: [PATCH 1/6] add test --- edg/parts/PowerConditioning.py | 2 +- edg/parts/test_diodemerge.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 edg/parts/test_diodemerge.py diff --git a/edg/parts/PowerConditioning.py b/edg/parts/PowerConditioning.py index bafaa8d69..4357f74af 100644 --- a/edg/parts/PowerConditioning.py +++ b/edg/parts/PowerConditioning.py @@ -45,7 +45,7 @@ def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = R class DiodePowerMerge(PowerConditioner, Block): """Diode power merge block for two voltage sources.""" - def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = (0, float("inf"))) -> None: + def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = RangeExpr.ALL) -> None: super().__init__() self.pwr_in1 = self.Port(VoltageSink(current_draw=RangeExpr())) diff --git a/edg/parts/test_diodemerge.py b/edg/parts/test_diodemerge.py new file mode 100644 index 000000000..1699fcff2 --- /dev/null +++ b/edg/parts/test_diodemerge.py @@ -0,0 +1,33 @@ +import unittest + +from .JlcDiode import * +from .. import DiodePowerMerge, BoardTop + + +class DiodeMergeTestTop(BoardTop): + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(DiodePowerMerge(voltage_drop=(0, 1) * Volt)) + (self.srca,), _ = self.chain(self.dut.pwr_in1, self.Block(DummyVoltageSource(voltage_out=(12, 14) * Volt))) + (self.srcb,), _ = self.chain(self.dut.pwr_in2, self.Block(DummyVoltageSource(voltage_out=(4, 5) * Volt))) + (self.sink,), _ = self.chain(self.dut.pwr_out, self.Block(DummyVoltageSink(current_draw=(0.5, 1.5) * Amp))) + + @override + def refinements(self) -> Refinements: + return Refinements( + class_refinements=[ + (Diode, CustomDiode), + ], + class_values=[ + (CustomDiode, ["footprint_spec"], "Diode_SMD:D_SOD-123"), + ], + ) + + +class DiodeMergeTestCase(unittest.TestCase): + def test_diode_merge(self) -> None: + compiled = ScalaCompiler.compile(DiodeMergeTestTop) + + self.assertEqual(compiled.get_value(["dut", "pwr_out", "voltage_out"]), Range(3.0, 14.0)) + self.assertEqual(compiled.get_value(["dut", "pwr_in1", "current_draw"]), Range(0.5, 1.5)) + self.assertEqual(compiled.get_value(["dut", "pwr_in2", "current_draw"]), Range(0.5, 1.5)) From b886a755f9d6581ca6e39fa906f09c8a9b17b812 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Wed, 20 May 2026 01:09:06 -0700 Subject: [PATCH 2/6] arbitrary diode generator --- edg/parts/PowerConditioning.py | 84 ++++++++++++++++++++-------------- edg/parts/test_diodemerge.py | 15 ++++-- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/edg/parts/PowerConditioning.py b/edg/parts/PowerConditioning.py index 4357f74af..705fc14b4 100644 --- a/edg/parts/PowerConditioning.py +++ b/edg/parts/PowerConditioning.py @@ -1,5 +1,5 @@ import warnings -from typing import Optional, cast, Any +from typing import Optional, Any from typing_extensions import override @@ -42,49 +42,63 @@ def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = R self.connect(self.pwr_out.net, self.pwr_in.net, self.diode.cathode) -class DiodePowerMerge(PowerConditioner, Block): - """Diode power merge block for two voltage sources.""" +class DiodePowerMerge(PowerConditioner, GeneratorBlock): + """Diode power merge block for multiple voltage sources.""" def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = RangeExpr.ALL) -> None: super().__init__() + self.voltage_drop = self.ArgParameter(voltage_drop) + self.reverse_recovery_time = self.ArgParameter(reverse_recovery_time) - self.pwr_in1 = self.Port(VoltageSink(current_draw=RangeExpr())) - self.pwr_in2 = self.Port(VoltageSink(current_draw=RangeExpr())) - self.pwr_out = self.Port( - VoltageSource( # use the spec voltage drop to avoid circular dependencies downstream - voltage_out=(self.pwr_in1.link().voltage - voltage_drop).hull( - self.pwr_in2.link().voltage - voltage_drop + self.pwr_ins = self.Port(Vector(VoltageSink.empty())) + self.pwr_out = self.Port(VoltageSource(voltage_out=RangeExpr())) + self.generator_param(self.pwr_ins.requested()) + + @override + def generate(self): + super().generate() + + input_hull = self.pwr_ins.map_extract(lambda pwr_in: pwr_in.link().voltage).hull() + requested = self.get(self.pwr_ins.requested()) + assert len(requested) > 0, "power inputs required" + + self.diodes = ElementDict[Diode]() + self.pwr_ins.defined() + for name in requested: + pwr_in = self.pwr_ins.append_elt(VoltageSink(current_draw=self.pwr_out.link().current_drawn), name) + self.diodes[name] = diode = self.Block( + Diode( + reverse_voltage=(0, pwr_in.link().voltage.upper() - input_hull.lower()), + current=self.pwr_out.link().current_drawn, + voltage_drop=self.voltage_drop, + reverse_recovery_time=self.reverse_recovery_time, ) ) - ) + self.connect(pwr_in.net, diode.anode) + self.connect(self.pwr_out.net, diode.cathode) - output_lower = ( - self.pwr_in1.link().voltage.lower().min(self.pwr_in2.link().voltage.lower()) - - RangeExpr._to_expr_type(voltage_drop).upper() - ) - self.diode1 = self.Block( - Diode( - reverse_voltage=(0, self.pwr_in1.link().voltage.upper() - output_lower), - current=self.pwr_out.link().current_drawn, - voltage_drop=voltage_drop, - reverse_recovery_time=reverse_recovery_time, + # use the spec voltage drop to avoid circular dependencies downstream + self.assign(self.pwr_out.voltage_out, input_hull - self.voltage_drop) + + def __getattr__(self, item: str) -> Any: + if item == "pwr_in1": + warnings.warn( + f"Use pwr_ins.request(...) instead.", + DeprecationWarning, + stacklevel=2, ) - ) - self.diode2 = self.Block( - Diode( - reverse_voltage=(0, self.pwr_in2.link().voltage.upper() - output_lower), - current=self.pwr_out.link().current_drawn, - voltage_drop=voltage_drop, - reverse_recovery_time=reverse_recovery_time, + return self.pwr_ins.request("1") + elif item == "pwr_in2": + warnings.warn( + f"Use pwr_ins.request(...) instead.", + DeprecationWarning, + stacklevel=2, ) - ) - - self.assign(self.pwr_in1.current_draw, self.pwr_out.link().current_drawn) - self.assign(self.pwr_in2.current_draw, self.pwr_out.link().current_drawn) - - self.connect(self.pwr_in1.net, self.diode1.anode) - self.connect(self.pwr_in2.net, self.diode2.anode) - self.connect(self.pwr_out.net, self.diode1.cathode, self.diode2.cathode) + return self.pwr_ins.request("2") + else: + raise AttributeError( + item + ) # ideally we'd use super().__getattr__(...), but that's not defined in base classes class PriorityPowerOr(PowerConditioner, KiCadSchematicBlock, Block): diff --git a/edg/parts/test_diodemerge.py b/edg/parts/test_diodemerge.py index 1699fcff2..482dd1baa 100644 --- a/edg/parts/test_diodemerge.py +++ b/edg/parts/test_diodemerge.py @@ -8,8 +8,12 @@ class DiodeMergeTestTop(BoardTop): def __init__(self) -> None: super().__init__() self.dut = self.Block(DiodePowerMerge(voltage_drop=(0, 1) * Volt)) - (self.srca,), _ = self.chain(self.dut.pwr_in1, self.Block(DummyVoltageSource(voltage_out=(12, 14) * Volt))) - (self.srcb,), _ = self.chain(self.dut.pwr_in2, self.Block(DummyVoltageSource(voltage_out=(4, 5) * Volt))) + (self.srca,), _ = self.chain( + self.dut.pwr_ins.request(), self.Block(DummyVoltageSource(voltage_out=(12, 14) * Volt)) + ) + (self.srcb,), _ = self.chain( + self.dut.pwr_ins.request(), self.Block(DummyVoltageSource(voltage_out=(4, 5) * Volt)) + ) (self.sink,), _ = self.chain(self.dut.pwr_out, self.Block(DummyVoltageSink(current_draw=(0.5, 1.5) * Amp))) @override @@ -29,5 +33,8 @@ def test_diode_merge(self) -> None: compiled = ScalaCompiler.compile(DiodeMergeTestTop) self.assertEqual(compiled.get_value(["dut", "pwr_out", "voltage_out"]), Range(3.0, 14.0)) - self.assertEqual(compiled.get_value(["dut", "pwr_in1", "current_draw"]), Range(0.5, 1.5)) - self.assertEqual(compiled.get_value(["dut", "pwr_in2", "current_draw"]), Range(0.5, 1.5)) + self.assertEqual(compiled.get_value(["dut", "pwr_ins", "0", "current_draw"]), Range(0.5, 1.5)) + self.assertEqual(compiled.get_value(["dut", "pwr_ins", "1", "current_draw"]), Range(0.5, 1.5)) + self.assertEqual(compiled.get_value(["dut", "diodes[0]", "fp_footprint"]), "Diode_SMD:D_SOD-123") + self.assertEqual(compiled.get_value(["dut", "diodes[1]", "fp_footprint"]), "Diode_SMD:D_SOD-123") + self.assertEqual(compiled.get_value(["dut", "diodes[2]", "fp_footprint"]), None) # doesn't exist From 7fd302b1a5fc0e6116679c5b231984174be08da3 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Wed, 20 May 2026 01:12:29 -0700 Subject: [PATCH 3/6] Update test_diodemerge.py --- edg/parts/test_diodemerge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edg/parts/test_diodemerge.py b/edg/parts/test_diodemerge.py index 482dd1baa..57e4ecf77 100644 --- a/edg/parts/test_diodemerge.py +++ b/edg/parts/test_diodemerge.py @@ -1,10 +1,10 @@ import unittest from .JlcDiode import * -from .. import DiodePowerMerge, BoardTop +from .PowerConditioning import DiodePowerMerge -class DiodeMergeTestTop(BoardTop): +class DiodeMergeTestTop(DesignTop): def __init__(self) -> None: super().__init__() self.dut = self.Block(DiodePowerMerge(voltage_drop=(0, 1) * Volt)) From ba2d228a6b0184d83932c1dc0468a1b8097b9510 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Wed, 20 May 2026 01:14:42 -0700 Subject: [PATCH 4/6] Update PowerConditioning.py --- edg/parts/PowerConditioning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edg/parts/PowerConditioning.py b/edg/parts/PowerConditioning.py index 705fc14b4..bfa4be6c7 100644 --- a/edg/parts/PowerConditioning.py +++ b/edg/parts/PowerConditioning.py @@ -55,7 +55,7 @@ def __init__(self, voltage_drop: RangeLike, reverse_recovery_time: RangeLike = R self.generator_param(self.pwr_ins.requested()) @override - def generate(self): + def generate(self) -> None: super().generate() input_hull = self.pwr_ins.map_extract(lambda pwr_in: pwr_in.link().voltage).hull() From 5b6328715fcc5fee00ae6141c63ba7f5d072fbfe Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Wed, 20 May 2026 01:26:21 -0700 Subject: [PATCH 5/6] fix and test the math --- edg/parts/PowerConditioning.py | 2 +- edg/parts/test_diodemerge.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/edg/parts/PowerConditioning.py b/edg/parts/PowerConditioning.py index bfa4be6c7..330a7c110 100644 --- a/edg/parts/PowerConditioning.py +++ b/edg/parts/PowerConditioning.py @@ -68,7 +68,7 @@ def generate(self) -> None: pwr_in = self.pwr_ins.append_elt(VoltageSink(current_draw=self.pwr_out.link().current_drawn), name) self.diodes[name] = diode = self.Block( Diode( - reverse_voltage=(0, pwr_in.link().voltage.upper() - input_hull.lower()), + reverse_voltage=(0, self.pwr_out.voltage_out.upper() - pwr_in.link().voltage.lower()), current=self.pwr_out.link().current_drawn, voltage_drop=self.voltage_drop, reverse_recovery_time=self.reverse_recovery_time, diff --git a/edg/parts/test_diodemerge.py b/edg/parts/test_diodemerge.py index 57e4ecf77..36d0819bf 100644 --- a/edg/parts/test_diodemerge.py +++ b/edg/parts/test_diodemerge.py @@ -36,5 +36,9 @@ def test_diode_merge(self) -> None: self.assertEqual(compiled.get_value(["dut", "pwr_ins", "0", "current_draw"]), Range(0.5, 1.5)) self.assertEqual(compiled.get_value(["dut", "pwr_ins", "1", "current_draw"]), Range(0.5, 1.5)) self.assertEqual(compiled.get_value(["dut", "diodes[0]", "fp_footprint"]), "Diode_SMD:D_SOD-123") + self.assertEqual(compiled.get_value(["dut", "diodes[0]", "current"]), Range(0.5, 1.5)) + self.assertEqual(compiled.get_value(["dut", "diodes[0]", "reverse_voltage"]), Range(0, 2.0)) self.assertEqual(compiled.get_value(["dut", "diodes[1]", "fp_footprint"]), "Diode_SMD:D_SOD-123") + self.assertEqual(compiled.get_value(["dut", "diodes[1]", "current"]), Range(0.5, 1.5)) + self.assertEqual(compiled.get_value(["dut", "diodes[1]", "reverse_voltage"]), Range(0, 10.0)) self.assertEqual(compiled.get_value(["dut", "diodes[2]", "fp_footprint"]), None) # doesn't exist From beb37520a2543bfd385969c36353e7efc6f4749d Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Wed, 20 May 2026 01:26:43 -0700 Subject: [PATCH 6/6] Update PowerConditioning.py --- edg/parts/PowerConditioning.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/edg/parts/PowerConditioning.py b/edg/parts/PowerConditioning.py index 330a7c110..1093c24db 100644 --- a/edg/parts/PowerConditioning.py +++ b/edg/parts/PowerConditioning.py @@ -59,6 +59,9 @@ def generate(self) -> None: super().generate() input_hull = self.pwr_ins.map_extract(lambda pwr_in: pwr_in.link().voltage).hull() + # use the spec voltage drop to avoid circular dependencies downstream + self.assign(self.pwr_out.voltage_out, input_hull - self.voltage_drop) + requested = self.get(self.pwr_ins.requested()) assert len(requested) > 0, "power inputs required" @@ -77,9 +80,6 @@ def generate(self) -> None: self.connect(pwr_in.net, diode.anode) self.connect(self.pwr_out.net, diode.cathode) - # use the spec voltage drop to avoid circular dependencies downstream - self.assign(self.pwr_out.voltage_out, input_hull - self.voltage_drop) - def __getattr__(self, item: str) -> Any: if item == "pwr_in1": warnings.warn(