Skip to content
Draft
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
26 changes: 24 additions & 2 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ tweak: # optional tweaking of .gv output
# <int/float> is assumed to be mm2
# <str> custom units and formats are allowed
# but unavailable for auto-conversion
show_equiv: <bool> # defaults to false; can auto-convert between mm2 and AWG
# and display the result when set to true
gauge_equiv: <gauge_equiv> # Can auto-convert between mm2 and AWG and display both when specified.
# Uses defaults from options.gauge_equiv when unset (see below).
length: <int/float>[ <unit>] # <int/float> is assumed to be in meters unless <unit> is specified
# e.g. length: 2.5 -> assumed to be 2.5 m
# or length: 2.5 ft -> "ft" is used as the unit
Expand Down Expand Up @@ -393,6 +393,28 @@ See [HTML Output Templates](../src/wireviz/templates/) for how metadata entries

# Character to split template and designator for autogenerated components
template_separator: <str> # Default = '.'

# Gauge equivalent default values (see below)
gauge_equiv: <gauge_equiv> # Default = False
```


## Gauge equivalents

Any cable entry might specify how to display a gauge equivalent value.
A single `<bool>` value will set the `show` attribute only.

Unset attributes will use the corresponding attribute from `options.gauge_equiv`.

```yaml
gauge_equiv: # This attribute accepts either a single <bool> or a dict with attributes shown below
show: <bool> # Show equivalent value if True, or not if False
rounding: <str> # nearest, thicker, or thinner
awg: <list> # List of available AWG values
mm2: <list/int> # List of available mm2 values or the number of digits in a computed value
match: # Rounding deviation limits in %
poor: <int> # Show ~equivalent when deviation is greater or equal this percentage
invalid: <int> # Avoid showing equivalent when deviation is greater or equal this percentage
```


Expand Down
7 changes: 3 additions & 4 deletions examples/demo02.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ templates: # defining templates to be used later on
- &wire_i2c
category: bundle
gauge: 0.14 mm2
gauge_equiv: true
colors: [BK, RD, YE, GN]

connectors:
Expand All @@ -55,22 +56,20 @@ cables:
W1:
<<: *wire_i2c
length: 0.2
show_equiv: true
W2:
<<: *wire_i2c
length: 0.4
show_equiv: true
W3:
category: bundle
gauge: 0.14 mm2
gauge_equiv: true
length: 0.3
colors: [BK, BU, OG, VT]
show_equiv: true
W4:
gauge: 0.25 mm2
gauge_equiv: true
length: 0.3
colors: [BK, RD]
show_equiv: true

connections:
-
Expand Down
2 changes: 1 addition & 1 deletion examples/ex01.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ cables:
color_code: IEC # auto-color wires based on a standard
wirecount: 4 # need to specify number of wires explicitly when using a color code
gauge: 0.25 mm2 # also accepts AWG as unit
show_equiv: true # auto-calculate AWG equivalent from metric gauge
gauge_equiv: true # auto-calculate AWG equivalent from metric gauge
length: 0.2 # length in m
shield: true
type: Serial
Expand Down
2 changes: 1 addition & 1 deletion examples/ex02.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ cables:
W1: &wire_power # define template
colors: [BK, RD] # number of wires implicit in color list
gauge: 0.25 # assume mm2 if no gauge unit is specified
show_equiv: true
gauge_equiv: true
length: 0.2
W2:
<<: *wire_power # create from template
Expand Down
2 changes: 1 addition & 1 deletion examples/ex03.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ cables:
wirecount: 6
colors: [BK, RD] # if number of items in color list is less than wirecount, loop colors
gauge: 0.25 mm2
show_equiv: true
gauge_equiv: true
length: 0.2

connections:
Expand Down
2 changes: 1 addition & 1 deletion examples/ex04.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cables:
W1:
gauge: 0.25 mm2
show_equiv: true
gauge_equiv: true
length: 0.2
color_code: IEC
wirecount: 6
Expand Down
184 changes: 182 additions & 2 deletions src/wireviz/DataClasses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

import math
from bisect import bisect_left, bisect_right
from dataclasses import InitVar, dataclass, field
from enum import Enum, auto
from pathlib import Path
Expand Down Expand Up @@ -47,6 +49,178 @@ class Metadata(dict):
pass


@dataclass
class MatchDeviationPercentage:
"""Match deviation limits in percentage for a poor and invalid match."""

poor: int = 10
invalid: int = 50

def __post_init__(self):
if any(not isinstance(v, int) for v in (self.poor, self.invalid)):
raise TypeError(
f"Both match.poor ({self.poor!r}) and match.invalid ({self.invalid!r}) are expected to be int"
)
if self.poor < 0:
raise ValueError(
f"A match.poor ({self.poor!r}) less than zero is not supported"
)
if self.invalid < self.poor:
raise ValueError(
f"A match.invalid ({self.invalid!r}) less than match.poor ({self.poor!r}) is not supported"
)

def check(self, found: Optional[float], target: float, equiv: str) -> str:
"""Check deviation between found and target value. Adjust equivalent accordingly."""
if found is None:
return "" # No match
deviationPercentage = abs(100.0 * (found - target) / target)
if deviationPercentage < self.poor:
return f" ({equiv})" # Good match
if deviationPercentage < self.invalid:
return f" (~{equiv})" # Poor match
return "" # Invalid match


@dataclass
class GaugeEquiv:
"""Gauge equivalence between mm2 and AWG."""

Rounding = Enum("Rounding", "NEAREST THICKER THINNER")

show: bool = True
rounding: Rounding = Rounding.NEAREST
awg: List[Union[str, int]] = tuple("0000 000 00".split()) + tuple(range(41))
mm2: Union[int, List[Union[str, float]]] = tuple(
"0.09 0.14 0.25 0.34 0.5 0.75 1 1.5 2.5 4 6 10 16 25 35 50".split()
)
match: MatchDeviationPercentage = field(default_factory=dict)

def __post_init__(self):
if not isinstance(self.rounding, self.Rounding):
try:
self.rounding = self.Rounding[self.rounding.upper()]
except AttributeError as e:
raise TypeError(
f"Expected rounding string, but got {self.rounding!r}: {e}"
) from e
except KeyError as e:
raise ValueError(f"Unexpected rounding value {self.rounding!r}") from e
self.match = MatchDeviationPercentage(**self.match)
# Stringify, deduplicate, and sort equiv lists. Make float-equiv pair lists.
try:
self.awg = sorted({str(e) for e in self.awg}, key=self.awg_n)
self._fmm2_sawg_pairs = sorted((self.awg_to_mm2(e), e) for e in self.awg)
except ValueError as e:
raise ValueError(f"Unexpexted awg entry: {e}") from e
if not isinstance(self.mm2, int):
try:
self.mm2 = sorted({str(e) for e in self.mm2}, key=float)
self._fmm2_smm2_pairs = sorted((float(e), e) for e in self.mm2)
except ValueError as e:
raise ValueError(f"Unexpexted mm2 entry: {e}") from e

@classmethod
def create(cls, input: Union[dict, bool], defaults: dict = {}):
"""Factory method accepting also optional defaults."""
if isinstance(input, bool):
input = {"show": input}
if not isinstance(input, dict):
raise TypeError(
f"Expected dict or bool as GaugeEquiv input, but got {input!r} ({type(input)})"
)
if not isinstance(defaults, dict):
raise TypeError(
f"Expected dict as GaugeEquiv defaults, but got {defaults!r} ({type(defaults)})"
)
input = dict(input) # Shallow copy to safely modify mutable dict
if input and "show" not in input:
input["show"] = True
if all(
"match" in d and isinstance(d["match"], dict) for d in (defaults, input)
):
input["match"] = {**defaults["match"], **input["match"]}
input = {**defaults, **input}
return cls(**input)

@staticmethod
def awg_n(awg: str) -> int:
"""Return numeric AWG or -1 for 00, -2 for 000, etc."""
if all(c == "0" for c in awg):
return 1 - len(awg)
if awg[-2:] == "/0":
return 1 - int(awg[:-2])
return int(awg)

@staticmethod
def awg_to_mm2(awg: str) -> float:
n = GaugeEquiv.awg_n(awg)
return math.pi * (0.005 * 25.4 / 2) ** 2 * 92 ** ((36 - n) / 19.5)

def find(
self, target: float, available: List[Tuple[float, str]]
) -> Tuple[Optional[float], str]:
"""Find a match for target in list of available float-equiv pairs."""
first = lambda x: x[0]
if self.rounding == self.Rounding.THINNER:
try:
i = bisect_right(available, target, key=first) - 1
except TypeError: # No key argument in Python < 3.10
i = bisect_right([first(e) for e in available], target) - 1
if i < 0:
return (None, "")
return available[i]
try:
i = bisect_left(available, target, key=first)
except TypeError: # No key argument in Python < 3.10
i = bisect_left([first(e) for e in available], target)
if self.rounding == self.Rounding.THICKER:
if i >= len(available):
return (None, "")
return available[i]
if self.rounding == self.Rounding.NEAREST:
# Check neighbors
candidates = []
if i < len(available):
candidates.append(available[i])
if i > 0:
candidates.append(available[i - 1])
if not candidates:
return (None, "")

# Pick the nearest
return min(candidates, key=lambda x: abs(x[0] - target))

raise ValueError(f"Invalid rounding value {self.rounding!r}")

def for_awg(self, value: str) -> str:
target = self.awg_to_mm2(value)
if isinstance(self.mm2, int):
if self.rounding == self.Rounding.NEAREST:
rounded_target = target
else:
assert target > 0
magnitude = math.floor(math.log10(target))
scale = 10 ** (self.mm2 - 1 - magnitude)
if self.rounding == self.Rounding.THICKER:
rounded_target = math.ceil(target * scale) / scale
elif self.rounding == self.Rounding.THINNER:
rounded_target = math.floor(target * scale) / scale
else:
raise ValueError(f"Invalid rounding value {self.rounding!r}")

equiv = f"{rounded_target:.{self.mm2}g}"
fmm2 = float(equiv)
else:
fmm2, equiv = self.find(target, self._fmm2_smm2_pairs)
return self.match.check(fmm2, target, equiv + " mm\u00b2")

def for_mm2(self, value: str) -> str:
target = float(value)
fmm2, equiv = self.find(target, self._fmm2_sawg_pairs)
return self.match.check(fmm2, target, equiv + " AWG")


@dataclass
class Options:
fontname: PlainText = "arial"
Expand All @@ -58,6 +232,7 @@ class Options:
color_mode: ColorMode = "SHORT"
mini_bom_mode: bool = True
template_separator: str = "."
gauge_equiv: GaugeEquiv = False

def __post_init__(self):
if not self.bgcolor_node:
Expand All @@ -68,6 +243,7 @@ def __post_init__(self):
self.bgcolor_cable = self.bgcolor_node
if not self.bgcolor_bundle:
self.bgcolor_bundle = self.bgcolor_cable
self.gauge_equiv = GaugeEquiv.create(self.gauge_equiv)


@dataclass
Expand Down Expand Up @@ -256,7 +432,7 @@ class Cable:
type: Optional[MultilineHypertext] = None
gauge: Optional[float] = None
gauge_unit: Optional[str] = None
show_equiv: bool = False
gauge_equiv: GaugeEquiv = field(default_factory=dict)
length: float = 0
length_unit: Optional[str] = None
color: Optional[Color] = None
Expand All @@ -273,7 +449,9 @@ class Cable:
ignore_in_bom: bool = False
additional_components: List[AdditionalComponent] = field(default_factory=list)

def __post_init__(self) -> None:
gauge_equiv_defaults: InitVar[GaugeEquiv] = {}

def __post_init__(self, gauge_equiv_defaults: GaugeEquiv) -> None:
if isinstance(self.image, dict):
self.image = Image(**self.image)

Expand Down Expand Up @@ -301,6 +479,8 @@ def __post_init__(self) -> None:
else:
pass # gauge not specified

self.gauge_equiv = GaugeEquiv.create(self.gauge_equiv, gauge_equiv_defaults)

if isinstance(self.length, str): # length and unit specified
try:
L, u = self.length.split(" ")
Expand Down
Loading
Loading