Skip to content

Commit 5a0ebcf

Browse files
Support Unitful in element construction (#93)
* Support `Unitful` in element contructors via package extension * Briefly mention Unitful support in documentation
1 parent 6fda63a commit 5a0ebcf

File tree

6 files changed

+262
-12
lines changed

6 files changed

+262
-12
lines changed

Project.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@ ProgressMeter = "0.6, 0.7, 0.8, 0.9, 1"
1919
StaticArrays = "0.8, 0.9, 0.10, 0.11, 0.12, 1.0"
2020
julia = "1.4"
2121

22+
[extensions]
23+
UnitfulExt = "Unitful"
24+
2225
[extras]
2326
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
2427
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
2528
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
29+
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
2630

2731
[targets]
28-
test = ["Test", "FFTW", "SparseArrays"]
32+
test = ["Test", "FFTW", "SparseArrays", "Unitful"]
33+
34+
[weakdeps]
35+
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"

docs/src/ug.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@
55
All circuit elements are created by calling corresponding functions; see the
66
[Element Reference](@ref) for details.
77

8+
### Unitful elements
9+
10+
ACME provides a package extension for
11+
[Unitful](https://github.com/PainterQubits/Unitful.jl) to support quantities
12+
with units when constructing elements. E.g. `resistor(4.7e3)` and
13+
`resistor(4.7u"kΩ")` are equivalent after `Unitful` has been loaded. This can
14+
increase readability and help catch bugs (e.g. `resistor(5u"V")` will throw an
15+
error). The input and output signals of the curcuit models will still be
16+
unitless, however.
17+
18+
!!! compat "Julia 1.9"
19+
Package extensions require Julia 1.9 or later. Consequently, unitful
20+
quantities are not supported on earlier Julia versions.
21+
822
## Circuit Description
923

1024
Circuits are described using `Circuit` instances, which are most easily created

ext/UnitfulExt.jl

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright 2023 Martin Holters
2+
# See accompanying license file.
3+
4+
module UnitfulExt
5+
6+
import ACME
7+
using Unitful: Unitful, @u_str, NoUnits, Quantity, Units, uconvert
8+
9+
remove_unit(unit::Units, q::Number) = NoUnits(uconvert(unit, q) / unit)
10+
11+
ACME.resistor(r::Quantity) = ACME.resistor(remove_unit(u"Ω", r))
12+
13+
ACME.potentiometer(r::Quantity, pos) = ACME.potentiometer(remove_unit(u"Ω", r), pos)
14+
ACME.potentiometer(r::Quantity) = ACME.potentiometer(remove_unit(u"Ω", r))
15+
16+
ACME.capacitor(c::Quantity) = ACME.capacitor(remove_unit(u"F", c))
17+
18+
ACME.inductor(l::Quantity) = ACME.inductor(remove_unit(u"H", l))
19+
20+
function ACME.transformer(
21+
l1::Quantity, l2::Quantity;
22+
coupling_coefficient=1,
23+
mutual_coupling::Quantity=coupling_coefficient*sqrt(l1*l2)
24+
)
25+
return ACME.transformer(
26+
remove_unit(u"H", l1), remove_unit(u"H", l2);
27+
mutual_coupling = remove_unit(u"H", mutual_coupling)
28+
)
29+
end
30+
31+
function ACME._transformer_ja(ns, α, c, kwargs::NamedTuple{K, <:Tuple{Vararg{Union{Real,Quantity}}}}) where {K}
32+
units = map(
33+
(k) -> begin
34+
if k === :D
35+
return u"m"
36+
elseif k === :A
37+
return u"m^2"
38+
elseif k === :a || k === :k || k === :Ms
39+
return u"A/m"
40+
else
41+
throw(ArgumentError("transformer: got unsupported keyword argument \"$(k)\""))
42+
end
43+
end,
44+
K
45+
)
46+
return ACME._transformer_ja(
47+
ns, α, c,
48+
NamedTuple{K}(map((unit, value) -> remove_unit(unit, value), units, values(kwargs))),
49+
)
50+
end
51+
52+
ACME.voltagesource(v::Quantity; rs::Quantity=0u"Ω") =
53+
ACME.voltagesource(remove_unit(u"V", v); rs=remove_unit(u"Ω", rs))
54+
ACME._voltagesource(rs::Quantity) = ACME._voltagesource(remove_unit(u"Ω", rs))
55+
56+
ACME.currentsource(i::Quantity; gp::Quantity=0u"S") =
57+
ACME.currentsource(remove_unit(u"A", i); gp=remove_unit(u"S", gp))
58+
ACME._currentsource(gp::Quantity) = ACME._currentsource(remove_unit(u"S", gp))
59+
60+
ACME._voltageprobe(gp::Quantity) = ACME._voltageprobe(remove_unit(u"S", gp))
61+
62+
ACME._currentprobe(rs::Quantity) = ACME._currentprobe(remove_unit(u"Ω", rs))
63+
64+
ACME._diode(is::Quantity, η::Real) = ACME._diode(remove_unit(u"A", is), η)
65+
66+
function ACME._bjt(typ, kwargs::NamedTuple{K, <:Tuple{Vararg{Union{Real,Quantity}}}}) where {K}
67+
units = map(
68+
(k) -> begin
69+
if k === :is || k === :isc || k === :ise || k === :ilc || k === :ile || k === :ikf || k === :ikr
70+
return u"A"
71+
elseif k === :vaf || k === :var
72+
return u"V"
73+
elseif k === :re || k === :rc || k === :rb
74+
return u"Ω"
75+
elseif k === :η || k === :ηc || k === :ηe || k === :βf || k === :βr || k === :ηcl || k === :ηel
76+
return NoUnits
77+
else
78+
throw(ArgumentError("bjt: got unsupported keyword argument \"$(k)\""))
79+
end
80+
end,
81+
K
82+
)
83+
return ACME._bjt(
84+
typ,
85+
NamedTuple{K}(map((unit, value) -> remove_unit(unit, value), units, values(kwargs))),
86+
)
87+
end
88+
89+
_mosfet_remove_units(unit::Units, q::Number) = remove_unit(unit, q)
90+
_mosfet_remove_units(unit::Units, q::Tuple{Vararg{Number,N}}) where {N} =
91+
ntuple(n -> remove_unit(unit / u"V"^(n-1), q[n]), Val(N))
92+
93+
function ACME._mosfet(typ, kwargs::NamedTuple{K, <:Tuple{Vararg{Union{Union{Real,Quantity},Tuple{Vararg{Union{Real,Quantity}}}}}}}) where {K}
94+
units = map(
95+
(k) -> begin
96+
if k === :vt
97+
return u"V"
98+
elseif k === :α
99+
return u"A/V^2"
100+
elseif k === :λ
101+
return u"V^-1"
102+
else
103+
throw(ArgumentError("bjt: got unsupported keyword argument \"$(k)\""))
104+
end
105+
end,
106+
K
107+
)
108+
return ACME._mosfet(
109+
typ,
110+
NamedTuple{K}(map((unit, q) -> _mosfet_remove_units(unit, q), units, values(kwargs))),
111+
)
112+
end
113+
114+
ACME.opamp(::Type{Val{:macak}}, gain::Real, vomin::Quantity, vomax::Quantity) =
115+
ACME.opamp(Val{:macak}, gain, remove_unit(u"V", vomin), remove_unit(u"V", vomax))
116+
117+
end

src/elements.jl

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2015, 2016, 2017, 2018, 2019, 2020, 2021 Martin Holters
1+
# Copyright 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2023 Martin Holters
22
# See accompanying license file.
33

44
export resistor, potentiometer, capacitor, inductor, transformer,
@@ -97,8 +97,12 @@ Magnetization"](http://dafx.de/paper-archive/2016/dafxpapers/08-DAFx-16_paper_10
9797
Pins: `1` and `2` for primary winding, `3` and `4` for secondary winding, and so
9898
on
9999
"""
100-
function transformer(::Type{Val{:JA}}; D=2.4e-2, A=4.54e-5, ns=[],
101-
a=14.1, α=5e-5, c=0.55, k=17.8, Ms=2.75e5)
100+
transformer(::Type{Val{:JA}}; ns=[], α=5e-5, c=0.55, kwargs...) =
101+
_transformer_ja(ns, α, c, (; kwargs...))
102+
_transformer_ja(ns, α, c, kwargs::NamedTuple{<:Any, <:Tuple{Vararg{Real}}}) =
103+
__transformer_ja(; ns=ns, α=α, c=c, kwargs...)
104+
function __transformer_ja(; D=2.4e-2, A=4.54e-5, ns=[],
105+
a=14.1, α=5e-5, c=0.55, k=17.8, Ms=2.75e5)
102106
μ0 = 1.2566370614e-6
103107
nonlinear_eq = @inline function (q)
104108
coth_q1 = coth(q[1])
@@ -175,7 +179,8 @@ Pins: `+` and `-` with `v` being measured from `+` to `-`
175179
"""
176180
function voltagesource end
177181
voltagesource(v; rs=0) = Element(mv=1, mi=-rs, u0=v, ports=[:+ => :-])
178-
voltagesource(; rs=0) = Element(mv=1, mi=-rs, mu=1, ports=[:+ => :-])
182+
voltagesource(; rs=0) = _voltagesource(rs)
183+
_voltagesource(rs) = Element(mv=1, mi=-rs, mu=1, ports=[:+ => :-])
179184

180185
"""
181186
currentsource(; gp=0)
@@ -190,7 +195,8 @@ Pins: `+` and `-` where `i` measures the current leaving source at the `+` pin
190195
"""
191196
function currentsource end
192197
currentsource(i; gp=0) = Element(mv=gp, mi=-1, u0=i, ports=[:+ => :-])
193-
currentsource(; gp=0) = Element(mv=gp, mi=-1, mu=1, ports=[:+ => :-])
198+
currentsource(; gp=0) = _currentsource(gp)
199+
_currentsource(gp) = Element(mv=gp, mi=-1, mu=1, ports=[:+ => :-])
194200

195201
"""
196202
voltageprobe()
@@ -201,7 +207,8 @@ defaults to zero.
201207

202208
Pins: `+` and `-` with the output voltage being measured from `+` to `-`
203209
"""
204-
voltageprobe(;gp=0) = Element(mv=-gp, mi=1, pv=1, ports=[:+ => :-])
210+
voltageprobe(;gp=0) = _voltageprobe(gp)
211+
_voltageprobe(gp) = Element(mv=-gp, mi=1, pv=1, ports=[:+ => :-])
205212

206213
"""
207214
currentprobe()
@@ -213,7 +220,8 @@ defaults to zero.
213220
Pins: `+` and `-` with the output current being the current entering the probe
214221
at `+`
215222
"""
216-
currentprobe(;rs=0) = Element(mv=1, mi=-rs, pi=1, ports=[:+ => :-])
223+
currentprobe(;rs=0) = _currentprobe(rs)
224+
_currentprobe(rs=0) = Element(mv=1, mi=-rs, pi=1, ports=[:+ => :-])
217225

218226
@doc raw"""
219227
diode(;is=1e-12, η = 1)
@@ -224,7 +232,8 @@ The reverse saturation current `is` has to be given in Ampere, the emission
224232
coefficient `η` is unitless.
225233

226234
Pins: `+` (anode) and `-` (cathode)
227-
""" diode(;is::Real=1e-12, η::Real = 1) =
235+
""" diode(;is=1e-12, η = 1) = _diode(is, η)
236+
_diode(is::Real, η::Real) =
228237
Element(mv=[1;0], mi=[0;1], mq=[-1 0; 0 -1], ports=[:+ => :-], nonlinear_eq =
229238
@inline function(q)
230239
v, i = q
@@ -295,7 +304,9 @@ The parameters are set using named arguments:
295304
| `rb` | Base terminal resistance
296305

297306
Pins: `base`, `emitter`, `collector`
298-
""" function bjt(typ; is=1e-12, η=1, isc=is, ise=is, ηc=η, ηe=η, βf=1000, βr=10,
307+
""" bjt(typ; kwargs...) = _bjt(typ, (; kwargs...))
308+
_bjt(typ, kwargs::NamedTuple{<:Any, <:Tuple{Vararg{Real}}}) = __bjt(typ; kwargs...)
309+
function __bjt(typ; is=1e-12, η=1, isc=is, ise=is, ηc=η, ηe=η, βf=1000, βr=10,
299310
ile=0, ilc=0, ηcl=ηc, ηel=ηe, vaf=Inf, var=Inf, ikf=Inf, ikr=Inf,
300311
re=0, rc=0, rb=0)
301312
local polarity
@@ -419,7 +430,10 @@ respectively. E.g. with `vt=(0.7, 0.1, 0.02)`, the $v_{GS}$-dpendent threshold
419430
voltage $v_T = 0.7 + 0.1\cdot v_{GS} + 0.02\cdot v_{GS}^2$ will be used.
420431

421432
Pins: `gate`, `source`, `drain`
422-
""" function mosfet(typ; vt=0.7, α=2e-5, λ=0)
433+
""" mosfet(typ; kwargs...) = _mosfet(typ, (; kwargs...))
434+
_mosfet(typ, kwargs::NamedTuple{<:Any, <:Tuple{Vararg{Union{Real,Tuple{Vararg{Real}}}}}}) =
435+
__mosfet(typ; kwargs...)
436+
function __mosfet(typ; vt=0.7, α=2e-5, λ=0)
423437
if typ == :n
424438
polarity = 1
425439
elseif typ == :p
@@ -519,7 +533,7 @@ connected to a ground node and has to provide the current sourced on the other
519533
output pin.
520534

521535
Pins: `in+` and `in-` for input, `out+` and `out-` for output
522-
""" function opamp(::Type{Val{:macak}}, gain, vomin, vomax)
536+
""" function opamp(::Type{Val{:macak}}, gain::Real, vomin::Real, vomax::Real)
523537
offset = 0.5 * (vomin + vomax)
524538
scale = 0.5 * (vomax - vomin)
525539
nonlinear_eq =

test/runtests.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,3 +794,7 @@ end
794794
# TODO: further validate y
795795
end
796796
end
797+
798+
if isdefined(Base, :get_extension) # Julia 1.10
799+
include("unitful.jl")
800+
end

test/unitful.jl

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Copyright 2023 Martin Holters
2+
# See accompanying license file.
3+
4+
using Unitful: @u_str, DimensionError
5+
6+
@testset "element constructors supporting Unitful" begin
7+
@test resistor(10) == resistor(10u"Ω")
8+
@test resistor(1000) == resistor(1u"kΩ")
9+
@test_throws DimensionError resistor(1u"kV")
10+
11+
@test potentiometer(10) == potentiometer(10u"Ω")
12+
@test potentiometer(1000) == potentiometer(1u"kΩ")
13+
@test_throws DimensionError potentiometer(1u"kV")
14+
@test potentiometer(10, 0.8) == potentiometer(10u"Ω", 0.8)
15+
@test potentiometer(1000, 0.8) == potentiometer(1u"kΩ", 0.8)
16+
@test_throws DimensionError potentiometer(1u"kV", 0.8)
17+
18+
@test capacitor(10) == capacitor(10u"F")
19+
@test capacitor(1e-9) == capacitor(1.0u"nF")
20+
@test_throws DimensionError capacitor(1u"kV")
21+
22+
@test inductor(10) == inductor(10u"H")
23+
@test inductor(1e-9) == inductor(1.0u"nH")
24+
@test_throws DimensionError inductor(1u"kV")
25+
26+
@test inductor(Val{:JA}; D=10e-3) == inductor(Val{:JA}; D=10.0u"mm")
27+
@test inductor(Val{:JA}; D=10e-3, A=45e-6, n=200, a=14, α=4e-5, c=0.5, k=17, Ms=275e3) ==
28+
inductor(Val{:JA}; D=10.0u"mm", A=45e-6u"m^2", n=200, a=14u"A/m", α=4e-5, c=0.5, k=17u"A/m", Ms=275.0u"kA/m")
29+
@test_throws DimensionError inductor(Val{:JA}; D=10.0u"mm^2")
30+
31+
@test transformer(10, 5) == transformer(10u"H", 5u"H")
32+
@test transformer(1e-9, 5e-9) == transformer(1.0u"nH", 5.0u"nH")
33+
@test transformer(4, 9; coupling_coefficient = 0.8) == transformer(4u"H", 9u"H"; coupling_coefficient = 0.8)
34+
@test transformer(1e-9, 5e-9; mutual_coupling = 2e-9) == transformer(1.0u"nH", 5.0u"nH"; mutual_coupling = 2.0u"nH")
35+
@test_throws DimensionError transformer(1u"kV", 2u"kV")
36+
@test_throws TypeError transformer(1u"nH", 2u"nH"; mutual_coupling = 2e-9)
37+
@test_throws DimensionError transformer(1u"nH", 2u"nH"; mutual_coupling = 2u"kV")
38+
39+
@test transformer(Val{:JA}; D=10e-3) == transformer(Val{:JA}; D=10.0u"mm")
40+
@test transformer(Val{:JA}; D=10e-3, A=45e-6, ns=[20], a=14, α=4e-5, c=0.5, k=17, Ms=275e3) ==
41+
transformer(Val{:JA}; D=10.0u"mm", A=45e-6u"m^2", ns=[20], a=14u"A/m", α=4e-5, c=0.5, k=17u"A/m", Ms=275.0u"kA/m")
42+
@test_throws DimensionError transformer(Val{:JA}; D=10.0u"mm^2")
43+
44+
@test voltagesource(10) == voltagesource(10u"V")
45+
@test voltagesource(1e-3) == voltagesource(1.0u"mV")
46+
@test voltagesource(5; rs=0.3) == voltagesource(5.0u"V"; rs=0.3u"Ω")
47+
@test voltagesource(; rs=0.3) == voltagesource(; rs=0.3u"Ω")
48+
@test_throws DimensionError voltagesource(1u"kA")
49+
@test_throws TypeError voltagesource(1u"V"; rs=1)
50+
@test_throws DimensionError voltagesource(1u"V"; rs=1u"V")
51+
@test_throws DimensionError voltagesource(; rs=1u"V")
52+
53+
@test currentsource(10) == currentsource(10u"A")
54+
@test currentsource(1e-3) == currentsource(1.0u"mA")
55+
@test currentsource(5; gp=0.3) == currentsource(5.0u"A"; gp=0.3u"S")
56+
@test currentsource(; gp=0.3) == currentsource(; gp=0.3u"S")
57+
@test_throws DimensionError currentsource(1u"kV")
58+
@test_throws TypeError currentsource(1u"A"; gp=1)
59+
@test_throws DimensionError currentsource(1u"A"; gp=1u"A")
60+
@test_throws DimensionError currentsource(; gp=1u"A")
61+
62+
@test voltageprobe(; gp=0.3) == voltageprobe(; gp=0.3u"S")
63+
@test_throws DimensionError voltageprobe(; gp=1u"A")
64+
65+
@test currentprobe(; rs=0.3) == currentprobe(; rs=0.3u"Ω")
66+
@test_throws DimensionError currentprobe(; rs=1u"V")
67+
68+
@test diode(; is=1e-15) == diode(; is=1.0u"fA")
69+
@test_throws DimensionError diode(; is=1u"V")
70+
@test_throws MethodError diode(; η=1u"V")
71+
72+
@test bjt(:npn; is=1e-15) == bjt(:npn; is=1.0u"fA")
73+
@test bjt(:npn; isc=2e-12, ise=3e-12, ηc=1.1, ηe=1.2, βf=1000, βr=10, ile=4e-15,
74+
ilc=5e-15, ηcl=1.15, ηel=1.18, vaf=40, var=30, ikf=5e-12, ikr=6e-12, re=1,
75+
rc=2, rb=3) ==
76+
bjt(:npn; isc=2e-12u"A", ise=3e-12u"A", ηc=1.1, ηe=1.2, βf=1000, βr=10,
77+
ile=4e-15u"A", ilc=5e-15u"A", ηcl=1.15, ηel=1.18, vaf=40u"V", var=30u"V",
78+
ikf=5e-12u"A", ikr=6e-12u"A", re=1u"Ω", rc=2u"Ω", rb=3u"Ω")
79+
@test_throws DimensionError bjt(:npn; is=1u"V")
80+
@test_throws DimensionError bjt(:npn; βr=1u"A")
81+
82+
@test mosfet(:n; vt=0.5) == mosfet(:n; vt=500.0u"mV")
83+
@test mosfet(:n; vt=0.5, α=25e-6, λ=0.1) ==
84+
mosfet(:n; vt=500.0u"mV", α=25.0e-6u"A/V^2", λ=0.1u"V^-1")
85+
@test mosfet(:n; vt=(-1.2454, -0.199, -0.0483), α=(0.0205, -0.0017), λ=0.1) ==
86+
mosfet(:n; vt=(-1.2454u"V", -0.199, -0.0483u"V^-1"), α=(0.0205u"A/V^2", -0.0017u"A/V^3"), λ=0.1u"V^-1")
87+
@test_throws DimensionError mosfet(:n; vt=500.0u"mA")
88+
@test_throws DimensionError mosfet(:n; vt=(500.0u"mV", 500.0u"mV"))
89+
90+
@test opamp(Val{:macak}, 10, -3, 5) == opamp(Val{:macak}, 10, -3u"V", 5u"V")
91+
@test_throws DimensionError opamp(Val{:macak}, 10, -3u"V", 5u"A")
92+
@test_throws MethodError opamp(Val{:macak}, 10, -3u"V", 5)
93+
@test_throws MethodError opamp(Val{:macak}, 10u"V", -3u"V", 5u"V")
94+
end

0 commit comments

Comments
 (0)