Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add analog signal range modeling #298

Merged
merged 26 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions compiler/src/main/scala/edg/compiler/ExprEvaluate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ object ExprEvaluate {
case (RangeValue(lhsMin, lhsMax), RangeValue(rhsMin, rhsMax)) =>
val all = Seq(lhsMin + rhsMin, lhsMin + rhsMax, lhsMax + rhsMin, lhsMax + rhsMax)
RangeValue(all.min, all.max)
case (RangeEmpty, RangeEmpty) => RangeEmpty
case (lhs: RangeValue, RangeEmpty) => lhs
case (RangeEmpty, rhs: RangeValue) => rhs
case (RangeValue(lhsMin, lhsMax), FloatPromotable(rhs)) =>
RangeValue(lhsMin + rhs, lhsMax + rhs)
case (FloatPromotable(lhs), RangeValue(rhsMin, rhsMax)) =>
Expand All @@ -35,6 +38,9 @@ object ExprEvaluate {
case (RangeValue(lhsMin, lhsMax), RangeValue(rhsMin, rhsMax)) =>
val all = Seq(lhsMin * rhsMin, lhsMin * rhsMax, lhsMax * rhsMin, lhsMax * rhsMax)
RangeValue(all.min, all.max)
case (RangeEmpty, RangeEmpty) => RangeEmpty
case (lhs: RangeValue, RangeEmpty) => RangeEmpty
case (RangeEmpty, rhs: RangeValue) => RangeEmpty
case (RangeValue(lhsMin, lhsMax), FloatPromotable(rhs)) if rhs >= 0 =>
RangeValue(lhsMin * rhs, lhsMax * rhs)
case (RangeValue(lhsMin, lhsMax), FloatPromotable(rhs)) if rhs < 0 =>
Expand Down Expand Up @@ -235,6 +241,7 @@ object ExprEvaluate {
case (Op.NEGATE, `val`) => `val` match {
case RangeValue(valMin, valMax) =>
RangeValue(-valMax, -valMin)
case RangeEmpty => RangeEmpty
case FloatValue(opVal) => FloatValue(-opVal)
case IntValue(opVal) => IntValue(-opVal)
case _ => throw new ExprEvaluateException(s"Unknown unary operand type in ${unary.op} ${`val`} from $unary")
Expand All @@ -243,6 +250,7 @@ object ExprEvaluate {
case (Op.INVERT, `val`) => `val` match {
case RangeValue(valMin, valMax) =>
RangeValue(1.0 / valMax, 1.0 / valMin)
case RangeEmpty => RangeEmpty
case FloatValue(opVal) => FloatValue(1.0 / opVal)
case IntValue(opVal) => IntValue(1 / opVal)
case _ => throw new ExprEvaluateException(s"Unknown unary operand type in ${unary.op} ${`val`} from $unary")
Expand Down
Binary file modified edg_core/resources/edg-compiler-precompiled.jar
Binary file not shown.
6 changes: 4 additions & 2 deletions electronics_abstract_parts/AbstractAnalogSwitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def __init__(self) -> None:
self.inputs = self.Port(Vector(AnalogSink.empty()))
self.out = self.Export(self.device.com.adapt_to(AnalogSource(
voltage_out=self.inputs.hull(lambda x: x.link().voltage),
signal_out=self.inputs.hull(lambda x: x.link().signal),
current_limits=self.device.analog_current_limits, # this device only, current draw propagated
impedance=self.device.analog_on_resistance + self.inputs.hull(lambda x: x.link().source_impedance)
)))
Expand All @@ -132,7 +133,7 @@ def generate(self):
self.inputs.defined()
for elt in self.get(self.inputs.requested()):
self.connect(
self.inputs.append_elt(AnalogSink().empty(), elt),
self.inputs.append_elt(AnalogSink.empty(), elt),
self.device.inputs.request(elt).adapt_to(AnalogSink(
voltage_limits=self.device.analog_voltage_limits, # this device only, voltages propagated
current_draw=self.out.link().current_drawn,
Expand Down Expand Up @@ -173,9 +174,10 @@ def generate(self):
self.outputs.defined()
for elt in self.get(self.outputs.requested()):
self.connect(
self.outputs.append_elt(AnalogSource().empty(), elt),
self.outputs.append_elt(AnalogSource.empty(), elt),
self.device.inputs.request(elt).adapt_to(AnalogSource(
voltage_out=self.input.link().voltage,
signal_out=self.input.link().signal,
current_limits=self.device.analog_current_limits, # this device only, voltages propagated
impedance=self.input.link().source_impedance + self.device.analog_on_resistance
)))
Expand Down
31 changes: 31 additions & 0 deletions electronics_abstract_parts/AbstractDiodes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Dict

from electronics_model import *
from .DummyDevices import ForcedAnalogVoltage
from .Categories import *
from .PartsTable import PartsTableColumn, PartsTableRow
from .PartsTablePart import PartsTableFootprintSelector
Expand Down Expand Up @@ -177,3 +178,33 @@ def contents(self):
current_draw=(0, 0)*Amp # TODO should be leakage current
)), self.pwr)
self.connect(self.diode.anode.adapt_to(Ground()), self.gnd)


class AnalogClampZenerDiode(Protection, KiCadImportableBlock):
"""Analog overvoltage protection diode to clamp the input voltage"""
@init_in_parent
def __init__(self, voltage: RangeLike):
super().__init__()

self.signal_in = self.Port(AnalogSink.empty(), [Input])
self.signal_out = self.Port(AnalogSource.empty(), [Output])
self.gnd = self.Port(Ground.empty(), [Common])

self.voltage = self.ArgParameter(voltage)

def contents(self):
super().contents()

self.diode = self.Block(ZenerDiode(zener_voltage=self.voltage))

self.forced = self.Block(ForcedAnalogVoltage(
forced_voltage=self.signal_in.link().voltage.intersect(
self.gnd.link().voltage + (0, self.diode.actual_zener_voltage.upper()))
))
self.connect(self.signal_in, self.forced.signal_in)
self.connect(self.signal_out, self.forced.signal_out, self.diode.cathode.adapt_to(AnalogSink()))
self.connect(self.diode.anode.adapt_to(Ground()), self.gnd)

def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]:
assert symbol_name == 'edg_importable:AnalogClampZenerDiode'
return {'IN': self.signal_in, 'OUT': self.signal_out, 'GND': self.gnd}
2 changes: 1 addition & 1 deletion electronics_abstract_parts/AbstractLed.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def __init__(self, color: LedColorLike = Led.Any, *, current_draw: RangeLike = (
self.current_draw = self.ArgParameter(current_draw)

self.signal = self.Port(DigitalSink.empty(), [InOut])
self.pwr = self.Port(VoltageSink().empty(), [Power])
self.pwr = self.Port(VoltageSink.empty(), [Power])


class IndicatorSinkLedResistor(IndicatorSinkLed):
Expand Down
4 changes: 2 additions & 2 deletions electronics_abstract_parts/AbstractResistor.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class PullupResistorArray(TypedTestPoint, GeneratorBlock):
def __init__(self, resistance: RangeLike):
super().__init__()
self.pwr = self.Port(VoltageSink.empty(), [Power])
self.io = self.Port(Vector(DigitalSingleSource().empty()), [InOut])
self.io = self.Port(Vector(DigitalSingleSource.empty()), [InOut])
self.generator_param(self.io.requested())
self.resistance = self.ArgParameter(resistance)

Expand All @@ -203,7 +203,7 @@ class PulldownResistorArray(TypedTestPoint, GeneratorBlock):
def __init__(self, resistance: RangeLike):
super().__init__()
self.gnd = self.Port(VoltageSink.empty(), [Common])
self.io = self.Port(Vector(DigitalSingleSource().empty()), [InOut])
self.io = self.Port(Vector(DigitalSingleSource.empty()), [InOut])
self.generator_param(self.io.requested())
self.resistance = self.ArgParameter(resistance)

Expand Down
1 change: 1 addition & 0 deletions electronics_abstract_parts/AbstractSolidStateRelay.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def __init__(self) -> None:
self.apull,
self.ic.fetb.adapt_to(AnalogSource(
voltage_out=self.ain.link().voltage,
signal_out=self.ain.link().signal,
current_limits=self.ic.load_current_limit,
impedance=self.ain.link().source_impedance + self.ic.load_resistance
)))
Expand Down
8 changes: 4 additions & 4 deletions electronics_abstract_parts/AbstractTestPoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class VoltageTestPoint(TypedTestPoint, Block):
"""Test point with a VoltageSink port."""
def __init__(self):
super().__init__()
self.io = self.Port(VoltageSink().empty(), [InOut])
self.io = self.Port(VoltageSink.empty(), [InOut])
self.tp = self.Block(TestPoint(name=self.io.link().name()))
self.connect(self.io, self.tp.io.adapt_to(VoltageSink()))

Expand All @@ -34,7 +34,7 @@ class DigitalTestPoint(TypedTestPoint, Block):
"""Test point with a DigitalSink port."""
def __init__(self):
super().__init__()
self.io = self.Port(DigitalSink().empty(), [InOut])
self.io = self.Port(DigitalSink.empty(), [InOut])
self.tp = self.Block(TestPoint(name=self.io.link().name()))
self.connect(self.io, self.tp.io.adapt_to(DigitalSink()))

Expand All @@ -47,7 +47,7 @@ class DigitalArrayTestPoint(TypedTestPoint, GeneratorBlock):
"""Creates an array of Digital test points, sized from the port array's connections."""
def __init__(self):
super().__init__()
self.io = self.Port(Vector(DigitalSink().empty()), [InOut])
self.io = self.Port(Vector(DigitalSink.empty()), [InOut])
self.generator_param(self.io.requested())

def generate(self):
Expand All @@ -62,7 +62,7 @@ class AnalogTestPoint(TypedTestPoint, Block):
"""Test point with a AnalogSink port."""
def __init__(self):
super().__init__()
self.io = self.Port(AnalogSink().empty(), [InOut])
self.io = self.Port(AnalogSink.empty(), [InOut])
self.tp = self.Block(TestPoint(name=self.io.link().name()))
self.connect(self.io, self.tp.io.adapt_to(AnalogSink()))

Expand Down
50 changes: 45 additions & 5 deletions electronics_abstract_parts/DummyDevices.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Dict

from electronics_model import *
from .Categories import *

Expand Down Expand Up @@ -52,12 +54,14 @@ def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL,
class DummyAnalogSink(DummyDevice):
@init_in_parent
def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL,
signal_limit: RangeLike = RangeExpr.ALL,
current_draw: RangeLike = RangeExpr.ZERO,
impedance: RangeLike = RangeExpr.INF) -> None:
super().__init__()

self.io = self.Port(AnalogSink(
voltage_limits=voltage_limit,
signal_limits=signal_limit,
current_draw=current_draw,
impedance=impedance
), [InOut])
Expand Down Expand Up @@ -88,17 +92,53 @@ def __init__(self, forced_voltage: RangeLike = RangeExpr()) -> None:
super().__init__()

self.pwr_in = self.Port(VoltageSink(
current_draw=RangeExpr(),
voltage_limits=RangeExpr()
current_draw=RangeExpr()
), [Input])

self.pwr_out = self.Port(VoltageSource(
voltage_out=forced_voltage,
current_limits=self.pwr_in.link().current_limits
voltage_out=forced_voltage
), [Output])

self.assign(self.pwr_in.current_draw, self.pwr_out.link().current_drawn)
self.assign(self.pwr_in.voltage_limits, self.pwr_out.link().voltage_limits)


class ForcedAnalogVoltage(DummyDevice, NetBlock):
@init_in_parent
def __init__(self, forced_voltage: RangeLike = RangeExpr()) -> None:
super().__init__()

self.signal_in = self.Port(AnalogSink(
current_draw=RangeExpr()
), [Input])

self.signal_out = self.Port(AnalogSource(
voltage_out=forced_voltage,
signal_out=self.signal_in.link().signal
), [Output])

self.assign(self.signal_in.current_draw, self.signal_out.link().current_drawn)


class ForcedAnalogSignal(KiCadImportableBlock, DummyDevice, NetBlock):
@init_in_parent
def __init__(self, forced_signal: RangeLike = RangeExpr()) -> None:
super().__init__()

self.signal_in = self.Port(AnalogSink(
current_draw=RangeExpr()
), [Input])

self.signal_out = self.Port(AnalogSource(
voltage_out=self.signal_in.link().voltage,
signal_out=forced_signal,
current_limits=self.signal_in.link().current_limits
), [Output])

self.assign(self.signal_in.current_draw, self.signal_out.link().current_drawn)

def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]:
assert symbol_name == 'edg_importable:Adapter'
return {'1': self.signal_in, '2': self.signal_out}


class ForcedDigitalSinkCurrentDraw(DummyDevice, NetBlock):
Expand Down
8 changes: 3 additions & 5 deletions electronics_abstract_parts/IdealIoController.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,13 @@ def generate(self) -> None:
self.adc.append_elt(AnalogSink(), elt)
self.dac.defined()
for elt in self.get(self.dac.requested()):
aout = self.dac.append_elt(AnalogSource(
voltage_out=self.gnd.link().voltage.hull(self.pwr.link().voltage)
), elt)
aout = self.dac.append_elt(AnalogSource.from_supply(self.gnd, self.pwr), elt)
io_current_draw_builder = io_current_draw_builder + (
aout.link().current_drawn.lower().min(0), aout.link().current_drawn.upper().max(0)
)

dio_model = DigitalBidir(
voltage_out=self.gnd.link().voltage.hull(self.pwr.link().voltage),
dio_model = DigitalBidir.from_supply(
self.gnd, self.pwr,
pullup_capable=True, pulldown_capable=True
)

Expand Down
7 changes: 3 additions & 4 deletions electronics_abstract_parts/MergedBlocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __init__(self) -> None:
super().__init__()
self.output = self.Port(AnalogSource(
voltage_out=RangeExpr(),
current_limits=RangeExpr.ALL, # limits checked in the link, this port is ideal
signal_out=RangeExpr(),
impedance=RangeExpr()
))
self.inputs = self.Port(Vector(AnalogSink.empty()))
Expand All @@ -91,13 +91,12 @@ def generate(self):
self.inputs.defined()
for in_request in self.get(self.inputs.requested()):
self.inputs.append_elt(AnalogSink(
voltage_limits=RangeExpr.ALL,
current_draw=self.output.link().current_drawn,
impedance=self.output.link().sink_impedance
), in_request)

self.assign(self.output.voltage_out,
self.inputs.hull(lambda x: x.link().voltage))
self.assign(self.output.voltage_out, self.inputs.hull(lambda x: x.link().voltage))
self.assign(self.output.signal_out, self.inputs.hull(lambda x: x.link().signal))
self.assign(self.output.impedance, # covering cases of any or all sources driving
self.inputs.hull(lambda x: x.link().source_impedance).hull(
1 / (1 / self.inputs.map_extract(lambda x: x.link().source_impedance)).sum()))
Expand Down
34 changes: 31 additions & 3 deletions electronics_abstract_parts/OpampCircuits.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from math import ceil, log10
from typing import List, Tuple, Dict

from electronics_abstract_parts import Resistor, Capacitor
from electronics_model import *
from .AbstractResistor import Resistor
from .AbstractCapacitor import Capacitor
from .ResistiveDivider import ResistiveDivider
from .AbstractOpamp import Opamp
from .Categories import OpampApplication
from .DummyDevices import ForcedAnalogSignal
from .ESeriesUtil import ESeriesRatioUtil, ESeriesUtil, ESeriesRatioValue


Expand All @@ -18,6 +21,12 @@ def __init__(self):
self.input = self.Port(AnalogSink.empty(), [Input])
self.output = self.Port(AnalogSource.empty(), [Output])

def contents(self):
super().contents()

self.amp = self.Block(Opamp())
self.forced = self.Block(ForcedAnalogSignal(self.input.link().signal))

self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"))


Expand Down Expand Up @@ -113,9 +122,16 @@ def generate(self) -> None:
impedance=self.r1.actual_resistance + self.r2.actual_resistance
)
reference_node: CircuitPort = self.reference
reference_range = self.reference.link().signal
else:
reference_type = Ground()
reference_node = self.gnd
reference_range = self.gnd.link().voltage

input_signal_range = self.amp.out.voltage_out.intersect(self.input.link().signal - reference_range)
output_range = input_signal_range * self.actual_amplification + reference_range
# TODO tolerances can cause the range to be much larger than actual, so bound it to avoid false-positives
self.forced = self.Block(ForcedAnalogSignal(self.amp.out.signal_out.intersect(output_range)))

self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"),
conversions={
Expand Down Expand Up @@ -236,13 +252,21 @@ def generate(self) -> None:
self.rf = self.Block(Resistor(Range.from_tolerance(rf_resistance, self.get(self.tolerance))))
self.rg = self.Block(Resistor(Range.from_tolerance(rf_resistance, self.get(self.tolerance))))

input_diff_range = self.input_positive.link().signal - self.input_negative.link().signal
output_diff_range = input_diff_range * self.actual_ratio + self.output_reference.link().signal
# TODO tolerances can cause the range to be much larger than actual, so bound it to avoid false-positives
self.forced = self.Block(ForcedAnalogSignal(self.amp.out.signal_out.intersect(output_diff_range)))

self.import_kicad(self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"),
conversions={
'r1.1': AnalogSink( # TODO very simplified and probably very wrong
impedance=self.r1.actual_resistance + self.rf.actual_resistance
),
'r1.2': AnalogSource( # combined R1 and Rf resistance
voltage_out=self.input_negative.link().voltage.hull(self.output.link().voltage),
voltage_out=ResistiveDivider.divider_output(
self.input_negative.link().voltage, self.amp.out.voltage_out,
ResistiveDivider.divider_ratio(self.r1.actual_resistance, self.rf.actual_resistance)
),
impedance=1 / (1 / self.r1.actual_resistance + 1 / self.rf.actual_resistance)
),
'rf.2': AnalogSink(), # ideal
Expand All @@ -253,7 +277,10 @@ def generate(self) -> None:
impedance=self.r2.actual_resistance + self.rg.actual_resistance
),
'r2.2': AnalogSource( # combined R2 and Rg resistance
voltage_out=self.input_positive.link().voltage.hull(self.output_reference.link().voltage),
voltage_out=ResistiveDivider.divider_output(
self.input_positive.link().voltage, self.output_reference.link().voltage,
ResistiveDivider.divider_ratio(self.r2.actual_resistance, self.rg.actual_resistance)
),
impedance=1 / (1 / self.r2.actual_resistance + 1 / self.rg.actual_resistance)
),
'rg.2': AnalogSink(), # ideal
Expand Down Expand Up @@ -368,6 +395,7 @@ def generate(self) -> None:
'c.1': AnalogSink(), # TODO impedance of the feedback circuit?

'r.2': AnalogSource(
voltage_out=self.amp.out.voltage_out,
impedance=self.r.actual_resistance
),
'c.2': AnalogSink(),
Expand Down
Loading