Skip to content

Commit c298616

Browse files
SimonePellegriniSimone PellegrinifhagemannSimone Pellegrini
authored
Refactor ThermalDiffusionLithiumDensity to remove hardcoded parameters (#578)
* Refactor ThermalDiffusionLithiumDensity to remove hardcoded parameters * Refactor ThermalDiffusionLithiumDensity to remove hardcoded parameters * Fix lithium diffusion model and YAML parsing * Bug fixes * Pass precision type to `ThermalDiffusionLithiumDensityParameters` * Store `ThermalDiffusionLithiumDensityParameters.H` in units of `cal` * Revert changes to `Project.toml` * Fix order of magnitude of `lithium_density_on_contact` The publication quotes values for calculating cm^-3, SSD expects m^-3 * cleaning the code and fixing bugs * Remove example JSON file for inverted coaxial detectors * Add references to `Thermal_Diffusion_Config.yaml` * Reorganize code and add docstrings * Remove `in_germanium` from `diffusivity` methods The publications used for germanium also seem to provide values for silicon. I do not see why this model should not be also applicable to silicon. * Update activation energy unit to `cal/mol` --------- Co-authored-by: Simone Pellegrini <simonepellegrini@Mac.fritz.box> Co-authored-by: Felix Hagemann <hagemann@berkeley.edu> Co-authored-by: Simone Pellegrini <simonepellegrini@MacBook-Air-di-Simone-4.fritz.box>
1 parent 7c4b370 commit c298616

File tree

5 files changed

+198
-26
lines changed

5 files changed

+198
-26
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
model: ThermalDiffusionLithiumDensity
2+
material: HPGe
3+
annealing_temperature_ranges:
4+
# https://doi.org/10.1103/PhysRev.96.21
5+
- T_min: 473K
6+
T_max: 873K
7+
D0: 25e-4cm^2/s
8+
H: 11800cal/mol
9+
# https://doi.org/10.1103/PhysRev.91.193
10+
- T_min: 873K
11+
T_max: 1273K
12+
D0: 13e-4cm^2/s
13+
H: 10700cal/mol
14+
15+
experimental_parameters:
16+
a: 21.27
17+
b: 2610
Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,49 @@
11
"""
2-
struct ThermalDiffusionLithiumDensity{T <: SSDFloat} <: AbstractImpurityDensity{T}
2+
struct ThermalDiffusionLithiumDensity{T <: SSDFloat} <: AbstractImpurityDensity{T}
33
44
Lithium impurity density model. Ref: [Dai _et al._ (2023)](https://doi.org/10.1016/j.apradiso.2022.110638)
55
66
## Fields
77
* `lithium_annealing_temperature::T`: lithium annealing temperature in Kelvin, when the lithium is diffused into the crystal. The default value is 623 K.
88
* `lithium_annealing_time::T`: lithium annealing time in seconds. The default value is 18 minutes.
99
* `distance_to_contact::Function`: the function for describing the depth to surface.
10-
1) use the built-in function `ConstructiveSolidGeometry.distance_to_surface` to calculate the distance to the surface of the doped contact
10+
1) use the built-in function `ConstructiveSolidGeometry.distance_to_surface` to calculate the distance to the surface of the doped contact.
1111
2) custom. Custom function might be much faster while the detector has good symmetry.
12-
* `lithium_density_on_contact::T`: the lithium concentration in the surface (in m^-3)
13-
* `lithium_diffusivity_in_germanium::T`: the diffusivity of lithium in germanium (in m^2*s^-1)
12+
* `lithium_density_on_contact::T`: the lithium concentration in the surface (in m^-3).
13+
* `lithium_diffusivity::T`: the diffusivity of lithium (in m^2/s).
1414
1515
## Parameters for constructing the model
1616
* `lithium_annealing_temperature::T`
1717
* `lithium_annealing_time::T`
1818
* `contact_with_lithium_doped::G`: the geometry of the contact with lithium doped, which is used to calculate the distance to the surface.
1919
- If you don't pass this parameter, the `distance_to_contact` function should be passed.
2020
- If you pass this parameter but don't pass the `distance_to_contact` function, the `distance_to_contact` function will use the default one.
21-
* `distance_to_contact::Function`: optional, default is `ConstructiveSolidGeometry.distance_to_surface(pt, contact_with_lithium_doped)`
22-
* `lithium_density_on_contact::T`: optional, default is the saturated lithium density at the given temperature
23-
* `lithium_diffusivity_in_germanium::T`: optional, default is calculated with the annealing temperature
21+
* `distance_to_contact::Function`: optional, default is `ConstructiveSolidGeometry.distance_to_surface(pt, contact_with_lithium_doped)`.
22+
* `lithium_density_on_contact::T`: optional, default is the saturated lithium density at the given temperature.
23+
* `lithium_diffusivity::T`: optional, default is calculated with the annealing temperature.
2424
"""
2525
struct ThermalDiffusionLithiumDensity{T <: SSDFloat} <: AbstractImpurityDensity{T}
2626
lithium_annealing_temperature::T
2727
lithium_annealing_time::T
2828
inactive_contact_id::Int
2929
distance_to_contact::Function
3030
lithium_density_on_contact::T
31-
lithium_diffusivity_in_germanium::T
31+
lithium_diffusivity::T
3232
end
3333

34-
function calculate_lithium_diffusivity_in_germanium(lithium_annealing_temperature::T)::T where {T <: SSDFloat}
35-
# D0 [m^2*s^-1]
36-
# H [cal]
37-
D0::T, H::T = ifelse(lithium_annealing_temperature <= 873, T.((2.5e-7, 11800)), T.((1.3e-7, 10700)))
38-
D0 * exp(-H/(R_gas*lithium_annealing_temperature))
39-
end
40-
function calculate_lithium_saturated_density(lithium_annealing_temperature::T)::T where {T <: SSDFloat}
41-
exp10(27.27 - 2610.0/lithium_annealing_temperature)
42-
end
34+
include("ThermalDiffusionLithiumDensityParameters.jl")
4335

44-
function ThermalDiffusionLithiumDensity{T}(
36+
function ThermalDiffusionLithiumDensity{T}(
4537
lithium_annealing_temperature::T,
4638
lithium_annealing_time::T,
4739
contact_with_lithium_doped::G,
4840
inactive_contact_id::Int;
41+
model_parameters::ThermalDiffusionLithiumDensityParameters{T} = ThermalDiffusionLithiumParameters(T=T),
4942
distance_to_contact::Function = pt::AbstractCoordinatePoint{T} -> ConstructiveSolidGeometry.distance_to_surface(pt, contact_with_lithium_doped),
50-
lithium_density_on_contact::T = calculate_lithium_saturated_density(lithium_annealing_temperature),
51-
lithium_diffusivity_in_germanium::T = calculate_lithium_diffusivity_in_germanium(lithium_annealing_temperature),
43+
lithium_density_on_contact::T = calculate_lithium_saturated_density(lithium_annealing_temperature, model_parameters.saturation),
44+
lithium_diffusivity::T = calculate_lithium_diffusivity(lithium_annealing_temperature, model_parameters.diffusion),
5245
) where {T <: SSDFloat, G <: Union{<:AbstractGeometry, Nothing}}
53-
ThermalDiffusionLithiumDensity{T}(lithium_annealing_temperature, lithium_annealing_time, inactive_contact_id, distance_to_contact, lithium_density_on_contact, lithium_diffusivity_in_germanium)
46+
ThermalDiffusionLithiumDensity{T}(lithium_annealing_temperature, lithium_annealing_time, inactive_contact_id, distance_to_contact, lithium_density_on_contact, lithium_diffusivity)
5447
end
5548

5649
function ImpurityDensity(T::DataType, t::Val{:li_diffusion}, dict::AbstractDict, input_units::NamedTuple)
@@ -59,15 +52,16 @@ function ImpurityDensity(T::DataType, t::Val{:li_diffusion}, dict::AbstractDict,
5952
contact_with_lithium_doped = haskey(dict, "contact_with_lithium_doped") ? dict["contact_with_lithium_doped"] : nothing # you don't have to pass the geometry of doped contact only when the distance_to_contact is passed
6053
inactive_contact_id = get(dict, "doped_contact_id", -1)
6154
inactive_contact_id < 1 && error("Invalid doped_contact_id: missing or misspelled key")
62-
ThermalDiffusionLithiumDensity{T}(lithium_annealing_temperature, lithium_annealing_time, contact_with_lithium_doped, inactive_contact_id)
55+
model_parameters = haskey(dict,"model_parameters") ? ThermalDiffusionLithiumParameters(dict["model_parameters"]; T) : ThermalDiffusionLithiumParameters(; T)
56+
ThermalDiffusionLithiumDensity{T}(lithium_annealing_temperature, lithium_annealing_time, contact_with_lithium_doped, inactive_contact_id, model_parameters)
6357
end
6458

6559
function get_impurity_density(li_diffusion::ThermalDiffusionLithiumDensity{T}, pt::AbstractCoordinatePoint{T})::T where {T <: SSDFloat}
6660
pt::CartesianPoint{T} = CartesianPoint(pt)
6761
depth = li_diffusion.distance_to_contact(pt)
68-
li_diffusion.lithium_density_on_contact * SpecialFunctions.erfc(depth/2/sqrt(li_diffusion.lithium_diffusivity_in_germanium*li_diffusion.lithium_annealing_time))
62+
li_diffusion.lithium_density_on_contact * SpecialFunctions.erfc(depth/2/sqrt(li_diffusion.lithium_diffusivity*li_diffusion.lithium_annealing_time))
6963
end
7064

71-
(*)(scale::Real, tidm::ThermalDiffusionLithiumDensity{T}) where {T} = ThermalDiffusionLithiumDensity{T}(tidm.lithium_annealing_temperature, tidm.lithium_annealing_time, tidm.inactive_contact_id, tidm.distance_to_contact, T(scale * tidm.lithium_density_on_contact), tidm.lithium_diffusivity_in_germanium)
65+
(*)(scale::Real, tidm::ThermalDiffusionLithiumDensity{T}) where {T} = ThermalDiffusionLithiumDensity{T}(tidm.lithium_annealing_temperature, tidm.lithium_annealing_time, tidm.inactive_contact_id, tidm.distance_to_contact, T(scale * tidm.lithium_density_on_contact), tidm.lithium_diffusivity)
7266

73-
(+)(offset::Union{<:Real, <:Quantity{<:Real, Unitful.𝐋^(-3)}}, tidm::ThermalDiffusionLithiumDensity{T}) where {T} = ThermalDiffusionLithiumDensity{T}(tidm.lithium_annealing_temperature, tidm.lithium_annealing_time, tidm.inactive_contact_id, tidm.distance_to_contact, T(to_internal_units(offset) + tidm.lithium_density_on_contact), tidm.lithium_diffusivity_in_germanium)
67+
(+)(offset::Union{<:Real, <:Quantity{<:Real, Unitful.𝐋^(-3)}}, tidm::ThermalDiffusionLithiumDensity{T}) where {T} = ThermalDiffusionLithiumDensity{T}(tidm.lithium_annealing_temperature, tidm.lithium_annealing_time, tidm.inactive_contact_id, tidm.distance_to_contact, T(to_internal_units(offset) + tidm.lithium_density_on_contact), tidm.lithium_diffusivity)
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""
2+
struct LithiumDiffusionParameters{T <: SSDFloat}
3+
4+
Set of parameters to calculate the lithium diffusivity `D` at a given annealing temperature `T_an` using
5+
```math
6+
D(T_\text{an}) = D_0 \exp(-H / RT+\text{an})
7+
```
8+
9+
## Parametric types
10+
* `T`: Precision type.
11+
12+
## Fields
13+
* `T_min::T`: Minimum temperature (in K) for which the parameters are valid.
14+
* `T_max::T`: Maximum temperature (in K) for which the parameters are valid.
15+
* `D0::T`: Diffusivity constant in m^2/s.
16+
* `H::T`: Activation energy in cal/mol.
17+
"""
18+
struct LithiumDiffusionParameters{T <: SSDFloat}
19+
T_min::T
20+
T_max::T
21+
D0::T
22+
H::T
23+
end
24+
25+
function calculate_lithium_diffusivity(lithium_annealing_temperature::T, parameters::NTuple{N, LithiumDiffusionParameters{T}})::T where {T <: SSDFloat, N}
26+
27+
if !(parameters[1].T_min lithium_annealing_temperature parameters[end].T_max)
28+
throw(ArgumentError("Invalid lithium_annealing_temperature=$(lithium_annealing_temperature): expected $(parameters[1].T_min) ≤ lithium_annealing_temperature ≤ $(parameters[end].T_max)."))
29+
end
30+
31+
for parameter in parameters
32+
if lithium_annealing_temperature <= parameter.T_max
33+
return parameter.D0 * exp(-parameter.H/(R_gas*lithium_annealing_temperature))
34+
end
35+
end
36+
37+
# throw an error if thediffusivity couldn't be calculated (this should never be the case)
38+
throw(ArgumentError("Could not determine lithium diffusivity"))
39+
end
40+
41+
42+
"""
43+
struct LithiumSaturationParameters{T <: SSDFloat}
44+
45+
Set of experimental fit parameters to calculate the lithium saturated density `N_s` in 1/cm³ at a given annealing temperature `T_an` using
46+
```math
47+
N_s(T_\text{an}) = 10^{a - b/T_\text{an})
48+
```
49+
50+
## Parametric types
51+
* `T`: Precision type.
52+
53+
## Fields
54+
* `a::T`: Fit parameter.
55+
* `b::T`: Fit parameter.`
56+
"""
57+
struct LithiumSaturationParameters{T <: SSDFloat}
58+
a::T
59+
b::T
60+
end
61+
62+
function calculate_lithium_saturated_density(lithium_annealing_temperature::T, parameters::LithiumSaturationParameters{T})::T where {T <: SSDFloat}
63+
ustrip(internal_length_unit^-3, exp10(parameters.a - parameters.b/lithium_annealing_temperature) * u"cm^-3")
64+
end
65+
66+
67+
"""
68+
ThermalDiffusionLithiumDensityParameters{T <: SSDFloat,N}
69+
70+
Set of parameters to calculate the lithium diffusivity and the lithium saturated density.
71+
72+
## Parametric types
73+
* `T`: Precision type.
74+
* `N`: Number of temperature ranges to calculate the lithium diffusivity.
75+
76+
## Fields
77+
* `diffusion::NTuple{N, LithiumDiffusionParameters{T}}`: Lithium diffusivity parameters.
78+
* `saturation::LithiumSaturationParameters{T}`: Lithium saturated density parameters.`
79+
80+
See also [`LithiumDiffusionParameters`](@ref) and [`LithiumSaturationParameters`](@ref).
81+
"""
82+
struct ThermalDiffusionLithiumDensityParameters{T <: SSDFloat,N}
83+
diffusion::NTuple{N, LithiumDiffusionParameters{T}}
84+
saturation::LithiumSaturationParameters{T}
85+
end
86+
87+
88+
function _ThermalDiffusionLithiumParameters(
89+
config::AbstractDict;
90+
T::Type = Float32,
91+
temperature_ranges::AbstractVector{<:AbstractDict} = config["annealing_temperature_ranges"] ,
92+
a::Union{Real, String} = config["experimental_parameters"]["a"],
93+
b::Union{Real, String} = config["experimental_parameters"]["b"],
94+
input_units::Union{Missing, NamedTuple} = missing
95+
)
96+
97+
temperature_unit = !ismissing(input_units) ? input_units.temperature : internal_temperature_unit
98+
diffusivity_unit = !ismissing(input_units) ? input_units.length^2 / internal_time_unit : internal_length_unit^2/internal_time_unit
99+
100+
diffusion_params = Tuple(
101+
LithiumDiffusionParameters{T}(
102+
_parse_value(T, r["T_min"], temperature_unit),
103+
_parse_value(T, r["T_max"], temperature_unit),
104+
_parse_value(T, r["D0"], diffusivity_unit),
105+
_parse_value(T, r["H"], internal_activation_energy_unit)
106+
)
107+
for r in temperature_ranges
108+
)
109+
110+
for i in eachindex(diffusion_params)
111+
if diffusion_params[i].T_max < diffusion_params[i].T_min
112+
throw(ConfigFileError("Invalid annealing temperature range $i: T_max must be ≥ T_min."))
113+
end
114+
if i > 1 && diffusion_params[i-1].T_max != diffusion_params[i].T_min
115+
throw(ConfigFileError("Annealing temperature ranges must be contiguous and increasing: range $(i-1) ends at a different temperature than range $i starts."))
116+
end
117+
end
118+
119+
saturation_params = LithiumSaturationParameters{T}(_parse_value(T, a, Unitful.NoUnits), _parse_value(T, b, Unitful.NoUnits))
120+
121+
ThermalDiffusionLithiumDensityParameters(diffusion_params, saturation_params)
122+
end
123+
124+
125+
function ThermalDiffusionLithiumParameters(config::AbstractDict, input_units::Union{Missing, NamedTuple} = missing; kwargs...)
126+
if !haskey(config, "annealing_temperature_ranges")
127+
throw(ConfigFileError("ThermalDiffusionLithiumDensity config file needs entry 'annealing_temperature_ranges'."))
128+
elseif isempty(config["annealing_temperature_ranges"])
129+
throw(ConfigFileError("ThermalDiffusionLithiumDensity config file cannot have an empty 'annealing_temperature_ranges'."))
130+
end
131+
132+
for (i, temperature_range) in enumerate(config["annealing_temperature_ranges"])
133+
for axis in ("T_min", "T_max", "D0", "H")
134+
if !haskey(temperature_range, axis)
135+
throw(ConfigFileError("ThermalDiffusionLithiumDensity config file needs entry 'annealing_temperature_ranges[$i]/$(axis)'."))
136+
end
137+
end
138+
for k in keys(temperature_range)
139+
if !(k in ("T_min", "T_max", "D0", "H"))
140+
@warn "Encountered unexpected key `$(k)` in 'annealing_temperature_ranges[$i]': Allowed keys are `T_min`, `T_max`, `D0` and `H`. Key `$(k)` will be ignored."
141+
end
142+
end
143+
end
144+
145+
if !haskey(config, "experimental_parameters")
146+
throw(ConfigFileError("ThermalDiffusionLithiumDensity config file needs entry 'experimental_parameters'."))
147+
elseif !haskey(config["experimental_parameters"], "a")
148+
throw(ConfigFileError("ThermalDiffusionLithiumDensity config file needs entry 'experimental_parameters/a'."))
149+
elseif !haskey(config["experimental_parameters"], "b")
150+
throw(ConfigFileError("ThermalDiffusionLithiumDensity config file needs entry 'experimental_parameters/b'."))
151+
end
152+
153+
_ThermalDiffusionLithiumParameters(config; input_units, kwargs...)
154+
end
155+
156+
const default_ThermalDiffusionLithiumDensity_config_file = joinpath(get_path_to_example_config_files(), "ThermalDiffusionLithiumDensity/Thermal_Diffusion_Config.yaml")
157+
function ThermalDiffusionLithiumParameters(config_filename::AbstractString = default_ThermalDiffusionLithiumDensity_config_file, input_units::Union{Missing, NamedTuple} = missing; kwargs...)
158+
ThermalDiffusionLithiumParameters(parse_config_file(config_filename), input_units; kwargs...)
159+
end

src/MaterialProperties/MaterialProperties.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const elementary_charge = Float64(ustrip(u"C", Unitful.q))
55
const ϵ0 = Float64(ustrip(u"F/m", Unitful.ϵ0))
66
const kB = Float64(ustrip(u"J/K", Unitful.k))
77
const me = Float64(ustrip(u"kg", Unitful.me))
8-
const R_gas = Float64(ustrip(u"cal/K/mol", Unitful.R))
8+
const R_gas = Float64(ustrip(internal_activation_energy_unit / internal_temperature_unit, Unitful.R))
99

1010
const material_properties = Dict{Symbol, NamedTuple}()
1111

src/Units.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const internal_diffusion_unit = internal_length_unit ^ 2 / internal_time_unit
1717
const internal_charge_density_unit = internal_charge_unit / internal_length_unit ^ 3
1818
const internal_charge_density_gradient_unit = internal_charge_unit / internal_length_unit ^ 4
1919
const internal_temperature_unit = u"K"
20+
const internal_activation_energy_unit = u"cal/mol"
2021

2122
const external_charge_unit = u"e_au" # elementary charge - from UnitfulAtomic.jl
2223

@@ -31,6 +32,7 @@ to_internal_units(x::Quantity{<:Real, dimension(internal_diffusion_unit)}) = us
3132
to_internal_units(x::Quantity{<:Real, dimension(internal_charge_density_unit)}) = ustrip(internal_charge_density_unit, x)
3233
to_internal_units(x::Quantity{<:Real, dimension(internal_charge_density_gradient_unit)}) = ustrip(internal_charge_density_gradient_unit, x)
3334
to_internal_units(x::Quantity{<:Real, dimension(internal_temperature_unit)}) = ustrip(internal_temperature_unit, x)
35+
to_internal_units(x::Quantity{<:Real, dimension(internal_activation_energy_unit)}) = ustrip(internal_activation_energy_unit, x)
3436

3537
from_internal_units(x::Real, unit::Unitful.Units{<:Any, dimension(internal_time_unit)}) = uconvert(unit, x * internal_time_unit)
3638
from_internal_units(x::Real, unit::Unitful.Units{<:Any, dimension(internal_voltage_unit)}) = uconvert(unit, x * internal_voltage_unit)

0 commit comments

Comments
 (0)