Skip to content

Add support for simulation for dead layer-Step3: BulkSurfaceConstantLifetimeChargeTrappingModel #480

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

Open
wants to merge 7 commits into
base: transition
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
55 changes: 54 additions & 1 deletion docs/src/man/charge_drift.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ In this case, the signal is calculated using the Schockley-Ramo theorem, i.e. by

### `BoggsChargeTrappingModel`

In SolidStateDetectors.jl, the only charge trapping model implemented so far is the one presented in [Boggs _et al._ (2023)](https://doi.org/10.1016/j.nima.2023.168756).
In SolidStateDetectors.jl, the Boggs charge trapping model implemented is the one presented in [Boggs _et al._ (2023)](https://doi.org/10.1016/j.nima.2023.168756).
In this model, the charge cloud loses part of its charge at every point `path[i]` of the charge drift, depending on its drift and thermal velocity, as well as the trapping product $[n\sigma_{e/h}]^{-1}$ for electrons and holes.
The charge signal is then given by the charge-decreased charge cloud reaching the contacts and the charges trapped on the way.

Expand Down Expand Up @@ -261,6 +261,59 @@ parameters = Dict("parameters" =>
sim.detector = SolidStateDetector(sim.detector, BoggsChargeTrappingModel{T}(parameters))
```

### `ConstantLifetimeTrappingModel`
This constant-lifetime-based charge trapping model is similar to the BoggsChargeTrappingModel, which is constant-mean-free-path based.
In this model, the charge cloud loses part of its charge at every point `path[i]` of the charge drift, depending on the lifetime.
The charge signal is then given by the charge-decreased charge cloud reaching the contacts and the charges trapped on the way.

This idea is presented in [Dai _et al._ (2023)](https://doi.org/10.1016/j.apradiso.2022.110638).

Besides, the model implemented has two sets of parameters for the sensitive (bulk) and inactive (dead layer/surface layer) volume respectively.
The inactive layer effect will only be considered if the corresponding `virtual_drift_volume` is defined in the configuration file.

The `ConstantLifetimeTrappingModel` can be applied in the configuration file by adding a field `charge_trapping_model` to the `semiconductor` with `model: ConstantLifetime`, `parameters` defining the constant lifetime and `inactive_layer_geometry`(optional) defining the inactive volume:
```yaml
detectors:
- semiconductor:
material: #...
geometry: #...
charge_trapping_model:
model: ConstantLifetime
parameters:
τh: 1ms
τe: 1ms
τh_inactive: 80ns
τe_inactive: 80ns
inactive_layer_geometry:
tube:
r:
from: 9.0
to: 10.0
h: 10.0
origin:
z: 5.0
```

The inactive layer could only be modeled with the help of the configuration file.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that still true? I saw in the tests that you can pass a geometry as inactive_layer to create a new SolidStateDetector

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can pass the geometry without the config, but the geometry has to be defined in the config.

But for the sensitive volume, the parameters can always be applied to an already read-in `SolidStateDetector` using for example:
```julia
using SolidStateDetectors, Unitful
T = Float64
sim = Simulation{T}(SSD_examples[:TrueCoaxial])
simulate!(sim)

# only model the sensitive volume
τh = τe = 1e6
parameters = Dict("parameters" => Dict("τh" => τh*u"ns", "τe" => τe*u"ns"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# only model the sensitive volume
τh = τe = 1e6
parameters = Dict("parameters" => Dict("τh" => τh*u"ns", "τe" => τe*u"ns"))
# only model the sensitive volume
τh = τe = 1e6*u"ns"
parameters = Dict("parameters" => Dict("τh" => τh, "τe" => τe))

Or maybe even do τh = τe = 1u"ms" if that also works


# model both the sensitive and inactive volumes based on the inactive volume geometry defined in the configuration file
τh = τe = 1e6
τh_inactive = τe_inactive = 80
parameters = Dict("parameters" => Dict("τh" => τh*u"ns", "τe" => τe*u"ns", "τh_inactive" => τh_inactive*u"ns", "τe_inactive" => τe_inactive*u"ns"), "inactive_layer_geometry" => sim.detector.semiconductor.charge_trapping_model.inactive_layer_geometry)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apply the units already to τh etc.:

Suggested change
τh = τe = 1e6
τh_inactive = τe_inactive = 80
parameters = Dict("parameters" => Dict("τh" => τh*u"ns", "τe" => τe*u"ns", "τh_inactive" => τh_inactive*u"ns", "τe_inactive" => τe_inactive*u"ns"), "inactive_layer_geometry" => sim.detector.semiconductor.charge_trapping_model.inactive_layer_geometry)
τh = τe = 1e6u"ns"
τh_inactive = τe_inactive = 80u"ns"
parameters = Dict("parameters" => Dict("τh" => τh, "τe" => τe, "τh_inactive" => τh_inactive, "τe_inactive" => τe_inactive), "inactive_layer_geometry" => sim.detector.semiconductor.charge_trapping_model.inactive_layer_geometry)


sim.detector = SolidStateDetector(sim.detector, ConstantLifetimeChargeTrappingModel{T}(parameters))
```


## Group Effects

Expand Down
31 changes: 23 additions & 8 deletions examples/example_config_files/public_TrueCoaxial.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,29 @@ detectors:
value: 1e8cm^-3
charge_drift_model:
include: ADLChargeDriftModel/drift_velocity_config.yaml
charge_trapping_model:
model: ConstantLifetime
parameters:
τh: 1ms
τe: 1ms
τh_inactive: 80ns
τe_inactive: 80ns
inactive_layer_geometry:
tube:
r:
from: 9.0
to: 10.0
h: 10.0
origin:
z: 5.0
geometry:
cone:
r:
from: 1.0
to: 10.0
h: 10
h: 10.0
origin:
z: 5
z: 5.0
contacts:
- name: "P+"
id: 1
Expand All @@ -49,10 +64,10 @@ detectors:
tube:
r:
from: 0.5
to: 1
h: 10
to: 1.0
h: 10.0
origin:
z: 5
z: 5.0
- name: "N+"
id: 2
material: HPGe
Expand All @@ -61,7 +76,7 @@ detectors:
tube:
r:
from: 10.0
to: 10.5
h: 10.
to: 10.0
h: 10.0
origin:
z: 5
z: 5.0
82 changes: 82 additions & 0 deletions src/ChargeTrapping/ChargeTrapping.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,85 @@ end




"""
struct ConstantLifetimeChargeTrappingModel{T <: SSDFloat} <: AbstractChargeTrappingModel{T}

This constant-lifetime-based charge trapping model is similar to the Boggs model, which is constant-mean-free-path based.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe define this without referencing Boggs here, something like

Suggested change
This constant-lifetime-based charge trapping model is similar to the Boggs model, which is constant-mean-free-path based.
This constant-lifetime-based charge trapping model assumes electrons and holes to have a constant free lifetime throughout the crystal.


The model implemented has two sets of parameters for the sensitive (bulk) and inactive (dead layer/surface layer) volume respectively.
The inactive layer effect will only be considered if the corresponding `virtual_drift_volume` is defined in the configuration file.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a virtual_drift_volume? You call it inactive_layer_geometry in the documentation.


## Fields
* `τh::T`: Lifetime for holes in bulk volume (sensitive region) (default: `τh = 1ms`).
* `τe::T`: Lifetime for electrons in bulk volume (default: `τe = 1ms`).
* `τh_inactive::T`: Lifetime for holes in surface layer (dead layer/inactive layer) (default: `τh_inactive = 1ms`).
* `τe_inactive::T`: Lifetime for electrons in surface layer (default: `τe_inactive = 1ms`).
* `inactive_layer_geometry`: The geometry of the inactive layer. When this is defined, the model will consider the inactive effect.
> Note: All τ must be much bigger than `Δt`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you explicitly check for this in the drift code? If not, it might make sense to throw a warning or error if τ is smaller than Δt.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it works like expected when τ is bigger than 10 ns, and it doesn't work when τ is 1 ns. I didn't test more values, but it already makes a lot of sense. When solving the difference equation, you should always make sure the step is small enough. But it is indeed better to throw a warning when τ and Δt are close.

Actually, the Boggs model also has this problem. The parameter (mean free path) should also be much bigger than the typical step length.


See also [Charge Trapping Models](@ref).
"""
struct ConstantLifetimeChargeTrappingModel{T <: SSDFloat} <: AbstractChargeTrappingModel{T}
τh::T
τe::T
τh_inactive::T
τe_inactive::T
inactive_layer_geometry
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if this works. You should give inactive_layer_geometry a type and add it as parametric type to the struct.

Suggested change
struct ConstantLifetimeChargeTrappingModel{T <: SSDFloat} <: AbstractChargeTrappingModel{T}
τh::T
τe::T
τh_inactive::T
τe_inactive::T
inactive_layer_geometry
end
struct ConstantLifetimeChargeTrappingModel{T <: SSDFloat, G <: AbstractGeometry} <: AbstractChargeTrappingModel{T}
τh::T
τe::T
τh_inactive::T
τe_inactive::T
inactive_layer_geometry::G
end

if it can also be missing, try something like:

Suggested change
struct ConstantLifetimeChargeTrappingModel{T <: SSDFloat} <: AbstractChargeTrappingModel{T}
τh::T
τe::T
τh_inactive::T
τe_inactive::T
inactive_layer_geometry
end
struct ConstantLifetimeChargeTrappingModel{T <: SSDFloat, G <: Union{<:AbstractGeometry, Missing} <: AbstractChargeTrappingModel{T}
τh::T
τe::T
τh_inactive::T
τe_inactive::T
inactive_layer_geometry::G
end

Maybe also consider nothing instead of missing here, if nothing is passed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I will try in this way: G <: Union{<:AbstractGeometry, nothing}.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful: here you would need a capital N because it’s a type, not a variable!

nothing => Nothing


function _calculate_signal(
ctm::ConstantLifetimeChargeTrappingModel{T},
path::AbstractVector{CartesianPoint{T}},
pathtimestamps::AbstractVector{T},
charge::T,
wpot::Interpolations.Extrapolation{T, 3},
S::CoordinateSystemType
)::Vector{T} where {T <: SSDFloat}

tmp_signal::Vector{T} = Vector{T}(undef, length(pathtimestamps))

running_sum::T = zero(T)
q::T = charge

inactive_layer_exist = ctm.inactive_layer_geometry !== missing
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
inactive_layer_exist = ctm.inactive_layer_geometry !== missing
inactive_layer_exist = !ismissing(ctm.inactive_layer_geometry)

Again, consider using nothing as default, you can then use !isnothing here.

τ::T = ifelse(charge > 0, ctm.τh, ctm.τe)
τ_inactive::T = ifelse(charge > 0, ctm.τh_inactive, ctm.τe_inactive)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a check here to see that τ is bigger than Δt?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

@inbounds for i in eachindex(tmp_signal)
Δt::T = (i > 1) ? (pathtimestamps[i] - pathtimestamps[i-1]) : zero(T)
Δq::T = (inactive_layer_exist && in(path[i], ctm.inactive_layer_geometry)) ? q * Δt/τ_inactive : q * Δt/τ
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split this into two lines so it's easier to see what is happening:

Suggested change
Δq::T = (inactive_layer_exist && in(path[i], ctm.inactive_layer_geometry)) ? q * Δt/τ_inactive : q * Δt/τ
τi::T = (inactive_layer_exist && in(path[i], ctm.inactive_layer_geometry)) ? τ_inactive : τ
Δq::T = q * Δt/τi

q -= Δq
w::T = i > 1 ? get_interpolation(wpot, path[i], S) : zero(T)
running_sum += w * Δq
tmp_signal[i] = running_sum + w * q
end

tmp_signal
end


ConstantLifetimeChargeTrappingModel(args...; T::Type{<:SSDFloat}, kwargs...) = ConstantLifetimeChargeTrappingModel{T}(args...; kwargs...)
function ConstantLifetimeChargeTrappingModel{T}(config_dict::AbstractDict = Dict()) where {T <: SSDFloat}
τh::T = ustrip(u"s", 1u"ms")
τe::T = ustrip(u"s", 1u"ms")
τh_inactive::T = ustrip(u"s", 80u"ns")
τe_inactive::T = ustrip(u"s", 80u"ns")

if haskey(config_dict, "model") && !haskey(config_dict, "parameters")
throw(ConfigFileError("`ConstantLifetimeChargeTrappingModel` does not have `parameters`"))
end

parameters = haskey(config_dict, "parameters") ? config_dict["parameters"] : config_dict
inactive_layer_geometry = haskey(config_dict, "inactive_layer_geometry") ? config_dict["inactive_layer_geometry"] : missing
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using nothing here as default.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using nothing here as default.

Suggested change
inactive_layer_geometry = haskey(config_dict, "inactive_layer_geometry") ? config_dict["inactive_layer_geometry"] : missing
inactive_layer_geometry = haskey(config_dict, "inactive_layer_geometry") ? config_dict["inactive_layer_geometry"] : nothing


allowed_keys = ("τh", "τe", "τh_inactive", "τe_inactive")
k = filter(k -> !(k in allowed_keys), keys(parameters))
!isempty(k) && @warn "The following keys will be ignored: $(k).\nAllowed keys are: $(allowed_keys)"

if haskey(parameters, "τh") τh = _parse_value(T, parameters["τh"], internal_time_unit) end
if haskey(parameters, "τe") τe = _parse_value(T, parameters["τe"], internal_time_unit) end
if haskey(parameters, "τh_inactive") τh_inactive = _parse_value(T, parameters["τh_inactive"], internal_time_unit) end
if haskey(parameters, "τe_inactive") τe_inactive = _parse_value(T, parameters["τe_inactive"], internal_time_unit) end
ConstantLifetimeChargeTrappingModel{T}(τh, τe, τh_inactive, τe_inactive, inactive_layer_geometry)
end
15 changes: 12 additions & 3 deletions src/SolidStateDetector/Semiconductor.jl
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,26 @@ function Semiconductor{T}(dict::AbstractDict, input_units::NamedTuple, outer_tra
T(293)
end

inner_transformations = parse_CSG_transformation(T, dict, input_units)
transformations = combine_transformations(inner_transformations, outer_transformations)
geometry = Geometry(T, dict["geometry"], input_units, transformations)

charge_trapping_model = if haskey(dict, "charge_trapping_model") &&
haskey(dict["charge_trapping_model"], "model") &&
dict["charge_trapping_model"]["model"] == "Boggs"
BoggsChargeTrappingModel{T}(dict["charge_trapping_model"], temperature = temperature)
elseif haskey(dict, "charge_trapping_model") &&
haskey(dict["charge_trapping_model"], "model") &&
dict["charge_trapping_model"]["model"] == "ConstantLifetime"
constant_lifetime_ctm_dict = deepcopy(dict["charge_trapping_model"])
if haskey(constant_lifetime_ctm_dict, "inactive_layer_geometry")
constant_lifetime_ctm_dict["inactive_layer_geometry"] = Geometry(T, dict["charge_trapping_model"]["inactive_layer_geometry"], input_units, transformations)
end
Comment on lines +96 to +98
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like this could be added to the constructor of ConstantLifetimeChargeTrappingModel?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly!

ConstantLifetimeChargeTrappingModel{T}(constant_lifetime_ctm_dict)
else
NoChargeTrappingModel{T}()
end

inner_transformations = parse_CSG_transformation(T, dict, input_units)
transformations = combine_transformations(inner_transformations, outer_transformations)
geometry = Geometry(T, dict["geometry"], input_units, transformations)
return Semiconductor(temperature, material, impurity_density_model, charge_drift_model, charge_trapping_model, geometry)
end

Expand Down
2 changes: 1 addition & 1 deletion src/SolidStateDetectors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export ElectricPotential, PointTypes, EffectiveChargeDensity, DielectricDistribu
export apply_initial_state!
export calculate_electric_potential!, calculate_weighting_potential!, calculate_electric_field!, calculate_drift_fields!
export ElectricFieldChargeDriftModel, ADLChargeDriftModel, IsotropicChargeDriftModel
export NoChargeTrappingModel, BoggsChargeTrappingModel
export NoChargeTrappingModel, BoggsChargeTrappingModel, ConstantLifetimeChargeTrappingModel
export get_active_volume, is_depleted, estimate_depletion_voltage
export calculate_stored_energy, calculate_mutual_capacitance, calculate_capacitance_matrix
export simulate_waveforms
Expand Down
40 changes: 39 additions & 1 deletion test/test_charge_drift_models.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ nbcc = NBodyChargeCloud(pos, Edep, 40, radius = T(0.0005), number_of_shells = 2)
@test isapprox( signalsum, T(2), atol = 5e-3 )
end

@timed_testset "Charge Trapping" begin
@timed_testset "Charge Trapping: BoggsChargeTrappingModel" begin
sim.detector = SolidStateDetector(sim.detector, BoggsChargeTrappingModel{T}())
evt = Event(pos, Edep)
timed_simulate!(evt, sim)
Expand Down Expand Up @@ -81,6 +81,44 @@ end
end
end

@timed_testset "Charge Trapping: ConstantLifetimeChargeTrappingModel" begin
signalsum_list = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, if you create an Array to push! to it afterwards, consider specifying a type

Suggested change
signalsum_list = []
signalsum_list = T[]

lifetime_list = 10 .^ (-3:0.3:0)
for lifetime in lifetime_list
parameters = Dict("parameters" => Dict("τh" => lifetime*u"ms", "τe" => lifetime*u"ms"))
trapping_model=ConstantLifetimeChargeTrappingModel{T}(parameters)
sim.detector = SolidStateDetector(sim.detector, trapping_model)
evt = Event(pos, Edep)
timed_simulate!(evt, sim)
signalsum = T(0)
for i in 1:length(evt.waveforms)
signalsum += abs(ustrip(evt.waveforms[i].signal[end]))
end
signalsum *= inv(ustrip(SolidStateDetectors._convert_internal_energy_to_external_charge(sim.detector.semiconductor.material)))
push!(signalsum_list, signalsum)
end

@info signalsum_list
@test all(signalsum_list .< T(2.0))
@test all(diff(signalsum_list) .> 0)

config_dict = SolidStateDetectors.parse_config_file(SSD_examples[:TrueCoaxial])
@testset "Parse config file 1" begin
config_dict["detectors"][1]["semiconductor"]["charge_trapping_model"] = Dict(
"model" => "ConstantLifetime",
"parameters" => Dict(
"τh" => "1ms",
"τe" => "1ms",
)
)
simA = @test_nowarn Simulation{T}(config_dict)
@test simA.detector.semiconductor.charge_trapping_model isa ConstantLifetimeChargeTrappingModel{T}
@test simA.detector.semiconductor.charge_trapping_model.τh == T(0.001)
@test simA.detector.semiconductor.charge_trapping_model.τe == T(0.001)
end

end

@timed_testset "Test completeness of charge drift models" begin
for c in InteractiveUtils.subtypes(AbstractChargeDriftModel)
if isstructtype(c)
Expand Down
Loading