From d531e6c58331aeb3826670fb2283434b30af1d0c Mon Sep 17 00:00:00 2001 From: Beforerr <58776897+Beforerr@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:06:24 -0800 Subject: [PATCH] feat: more exhaustive type hierarchy (#4) * feat: inner constructor for Particle * feat: Add CustomParticle * doc: Update particle type hierarchy to add CustomParticle * refactor: more exhaustive type hierarchy * refactor: Update special particles type hierarchy - Make _special_particles.jl public visible, add Positron * refactor: Rename ChargedParticleImpl to Particle and update related code The `Particle` type now represents the concrete implementation for storing particle properties. * refactor: rename `Particle` interface for creating particles to `particle`, and add functionality to process special particles * doc: update `particle` * chore: more aliases --- docs/make.jl | 1 + docs/src/api.md | 2 +- docs/src/index.md | 8 +- docs/src/manual/particle-types.md | 21 ++++- src/ChargedParticles.jl | 11 +-- src/_special_particles.jl | 11 --- src/aliases.jl | 23 +++--- src/custom_particles.jl | 36 +++++++++ src/particle.jl | 73 ++++++++++++++++++ src/properties.jl | 22 +++--- src/special_particles.jl | 38 ++++++++++ src/types.jl | 122 ++++++++++++------------------ src/utils.jl | 4 +- test/custom_particles.jl | 15 ++++ test/runtests.jl | 8 +- test/types.jl | 9 +++ 16 files changed, 277 insertions(+), 127 deletions(-) delete mode 100644 src/_special_particles.jl create mode 100644 src/custom_particles.jl create mode 100644 src/particle.jl create mode 100644 src/special_particles.jl create mode 100644 test/custom_particles.jl diff --git a/docs/make.jl b/docs/make.jl index 0482f56..747c83f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -15,6 +15,7 @@ makedocs( ], "API Reference" => "api.md" ], + checkdocs=:exports, doctest=true ) diff --git a/docs/src/api.md b/docs/src/api.md index 2e869e5..882d0bd 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -11,7 +11,7 @@ Order = [:type] ## Constructors ```@docs -Particle +particle proton electron ``` diff --git a/docs/src/index.md b/docs/src/index.md index 92a33c7..6c07440 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -35,15 +35,15 @@ p = proton() # proton α = Particle("alpha") # alpha particle # Create ions and isotopes -fe3 = Particle("Fe3+") # Iron(III) ion -fe56 = Particle("Fe-56") # Iron-56 +fe = Particle("Fe-56 3+") # Iron(III) ion +fe54 = Particle(:Fe, 3, 54) d = Particle("D+") # Deuteron # Access properties println("Electron mass: ", mass(e)) println("Alpha particle charge: ", charge(α)) -println("Iron charge: ", charge(fe3)) -println("Iron-56 mass number: ", mass_number(fe56)) +println("Iron(III) ion charge: ", charge(fe)) +println("Iron-54 mass number: ", mass_number(fe54)) println("Deuteron mass: ", mass(d)) ``` diff --git a/docs/src/manual/particle-types.md b/docs/src/manual/particle-types.md index 0f7fbb7..80ac3fb 100644 --- a/docs/src/manual/particle-types.md +++ b/docs/src/manual/particle-types.md @@ -5,19 +5,32 @@ ChargedParticles.jl provides a flexible type system for representing various typ ## Type Hierarchy ```@raw html + +The package uses a exhaustive type hierarchy: +
 AbstractParticle
-└── ChargedParticleImpl
+├── AbstractChargeParticle
+│   ├── Particle
+├── AbstractFermion
+│   ├── AbstractLepton
+│   │   ├── Electron
+│   │   └── Muon
+│   └── AbstractQuark
+│   └── Neutron
+│   └── ...
+└── CustomParticle
 
``` -The package uses a simple two-level type hierarchy: - `AbstractParticle`: Base abstract type for all particles -- `ChargedParticleImpl`: Concrete implementation storing particle properties +- `AbstractChargeParticle`: Particles that could carry an electric charge. +- `Particle`: Physically meaningful particle type (for ions where symbol encodes the actual type of the particle) +- `CustomParticle`: Custom particle type for user-defined particles (where symbol is just a label) ## Particle Properties -Each particle has three fundamental properties: +Each particle (`Particle`) has three fundamental properties: 1. **Symbol** (`symbol::Symbol`): Chemical symbol or particle identifier - Regular elements: `:Fe`, `:He`, etc. diff --git a/src/ChargedParticles.jl b/src/ChargedParticles.jl index 67af0d3..968ed8e 100644 --- a/src/ChargedParticles.jl +++ b/src/ChargedParticles.jl @@ -6,17 +6,18 @@ using Mendeleev: elements # for PeriodicTable compatibility using Match using Unitful: me, mp -const ELEMENTARY_PARTICLES = (:e, :μ, :n) - include("./types.jl") +include("./particle.jl") include("./properties.jl") include("./aliases.jl") include("./utils.jl") include("./display.jl") -include("./_special_particles.jl") +include("./custom_particles.jl") +include("./special_particles.jl") -export AbstractParticle, Particle, ChargedParticleImpl +export AbstractParticle, Particle, CustomParticle +export Electron, Muon, Neutron export mass, charge, charge_number, atomic_number, mass_number, element, mass_energy export is_ion, is_chemical_element, is_default_isotope -export electron, proton +export particle, electron, proton end \ No newline at end of file diff --git a/src/_special_particles.jl b/src/_special_particles.jl deleted file mode 100644 index ce55cb2..0000000 --- a/src/_special_particles.jl +++ /dev/null @@ -1,11 +0,0 @@ -const leptons = ("e-", "mu-", "tau-", "nu_e", "nu_mu", "nu_tau") -const antileptons = ("e+", "mu+", "tau+", "anti_nu_e", "anti_nu_mu", "anti_nu_tau") -const baryons = ("p+", "n") -const antibaryons = ("p-", "antineutron") - -const mass_dicts = Dict( - :e => me, - :μ => 206.7682827me, - :p => mp, - :n => Unitful.mn, -) \ No newline at end of file diff --git a/src/aliases.jl b/src/aliases.jl index 2973ef4..a480959 100644 --- a/src/aliases.jl +++ b/src/aliases.jl @@ -2,6 +2,10 @@ const ELECTRON_ALIASES = ("electron", "e-", "e") const PROTON_ALIASES = ("proton", "p+", "p", "H+") +const POSITRON_ALIASES = ("positron", "e+") +const NEUTRON_ALIASES = ("neutron", "n") +const MUON_ALIASES = ("muon", "μ", "μ-", "mu-", "mu") + """ PARTICLE_ALIASES @@ -10,10 +14,11 @@ Dictionary of common particle aliases and their corresponding (symbol, charge, m Each entry maps a string alias to a tuple of (symbol, charge, mass_number) """ PARTICLE_ALIASES = Dict( - "e+" => ("e", 1, 0), - "positron" => ("e", 1, 0), - "neutron" => ("n", 0, 1), - "n" => ("n", 0, 1), + (PROTON_ALIASES .=> Ref(("H", 1, 1)))..., + (ELECTRON_ALIASES .=> :Electron)..., + (NEUTRON_ALIASES .=> :Neutron)..., + (POSITRON_ALIASES .=> :Positron)..., + (MUON_ALIASES .=> :Muon)..., "alpha" => ("He", 2, 4), "deuteron" => ("H", 1, 2), "D+" => ("H", 1, 2), @@ -21,12 +26,8 @@ PARTICLE_ALIASES = Dict( "T" => ("H", 0, 3), "triton" => ("H", 1, 3), "T+" => ("H", 1, 3), - "mu-" => ("μ", -1, 0), - "muon" => ("μ", -1, 0), + "mu-" => :Muon, + "muon" => :Muon, "antimuon" => ("μ", 1, 0), "mu+" => ("μ", 1, 0), -) - -ELECTRON_ALIASES_DICT = Dict(str => ("e", -1, 0) for str in ELECTRON_ALIASES) -PROTON_ALIASES_DICT = Dict(str => ("H", 1, 1) for str in PROTON_ALIASES) -PARTICLE_ALIASES = merge(PARTICLE_ALIASES, ELECTRON_ALIASES_DICT, PROTON_ALIASES_DICT) \ No newline at end of file +) \ No newline at end of file diff --git a/src/custom_particles.jl b/src/custom_particles.jl new file mode 100644 index 0000000..f378ae9 --- /dev/null +++ b/src/custom_particles.jl @@ -0,0 +1,36 @@ +""" + CustomParticle <: AbstractParticle + +A particle with user-defined mass and charge. + +# Fields +- `mass`: The mass of the particle (can be any numeric type or Unitful quantity) +- `charge_number`: Integer representing the charge state +- `symbol`: Optional symbol identifier (defaults to nothing) + +# Examples +```julia +CustomParticle(1.67e-27u"kg", 1) # A particle with proton-like mass and +1 charge +CustomParticle(2.0u"GeV", -1, :custom) # A custom particle with specified symbol +``` +""" +@kwdef struct CustomParticle <: AbstractParticle + mass::Unitful.Mass + charge_number::Int + symbol::Symbol + function CustomParticle(mass, charge_number, symbol=:custom) + new(mass, charge_number, symbol) + end +end + +function CustomParticle(mass_energy::Unitful.Energy, charge_number, symbol=:custom) + mass = uconvert(u"kg", mass_energy / Unitful.c^2) + CustomParticle(mass, charge_number, symbol) +end + +# Override mass method for CustomParticle +mass(p::CustomParticle) = p.mass +# CustomParticle doesn't have a mass number +mass_number(::CustomParticle) = nothing +# CustomParticle doesn't have an element +element(::CustomParticle) = nothing \ No newline at end of file diff --git a/src/particle.jl b/src/particle.jl new file mode 100644 index 0000000..bf181d8 --- /dev/null +++ b/src/particle.jl @@ -0,0 +1,73 @@ +""" + particle(str::AbstractString; mass_numb=nothing, z=nothing) + +Create a particle from a string representation. + +# Arguments +- `str::AbstractString`: String representation of the particle + +# String Format Support +- Element symbols: `"Fe"`, `"He"` +- Isotopes: `"Fe-56"`, `"D"` +- Ions: `"Fe2+"`, `"H-"` +- Common aliases: `"electron"`, `"proton"`, `"alpha"`, `"mu-"` + +# Examples +```jldoctest; output = false +# Elementary particles +electron = particle("e-") +muon = particle("mu-") +positron = particle("e+") + +# Ions and isotopes +proton = particle("H+") +alpha = particle("He2+") +deuteron = particle("D+") +iron56 = particle("Fe-56") +# output +Fe +``` +""" +function particle(str::AbstractString; mass_numb=nothing, z=nothing) + # Check aliases first + if haskey(PARTICLE_ALIASES, str) + result = PARTICLE_ALIASES[str] + if result isa Tuple + symbol, charge, mass_number = result + return Particle(Symbol(symbol), charge, mass_number) + else + return eval(result)() + end + end + + # Try to parse as element with optional mass number and charge + result = parse_particle_string(str) + if !isnothing(result) + (symbol, parsed_charge, parsed_mass_numb) = result + element = elements[symbol] + charge = determine(parsed_charge, z; default=0) + mass_number = determine(parsed_mass_numb, mass_numb; default=element.mass_number) + return Particle(symbol, charge, mass_number) + end + throw(ArgumentError("Invalid particle string format: $str")) +end + +""" + particle(sym::Symbol) + +Create a particle from its symbol representation. + +# Examples +```jldoctest; output = false +# Elementary particles +electron = particle(:e) +muon = particle(:muon) +proton = particle(:p) +# output +H⁺ +``` +""" +particle(sym::Symbol; kwargs...) = particle(string(sym); kwargs...) + +Particle(str::AbstractString; mass_numb=nothing, z=nothing) = particle(str; mass_numb, z) +Particle(sym::Symbol; kwargs...) = particle(string(sym); kwargs...) \ No newline at end of file diff --git a/src/properties.jl b/src/properties.jl index 6029e92..5c5279f 100644 --- a/src/properties.jl +++ b/src/properties.jl @@ -1,4 +1,4 @@ -const calculated_properties = (:charge, :atomic_number, :element, :mass_energy, :mass) +const calculated_properties = (:charge_number, :charge, :atomic_number, :element, :mass_energy, :mass, :symbol) const properties_fn_map = Dict() const synonym_properties = Dict( :A => :mass_number, @@ -22,10 +22,8 @@ end # Basic properties """Return the mass of the particle""" function mass(p::AbstractParticle) - get(mass_dicts, p.symbol) do - base_mass = mass(p.element, p.mass_number) - return base_mass - p.charge_number * Unitful.me - end + base_mass = mass(p.element, p.mass_number) + return base_mass - p.charge_number * Unitful.me end charge_number(p::AbstractParticle) = p.charge_number @@ -68,10 +66,10 @@ println(mass_number(e)) # 0 mass_number(p) = p.mass_number mass_number(::Nothing) = nothing -function element(p::AbstractParticle) +element(::AbstractParticle) = nothing + +function element(p::Particle) @match p.symbol begin - x, if x in ELEMENTARY_PARTICLES - end => return nothing :p => return elements[:H] _ => return elements[p.symbol] end @@ -79,10 +77,12 @@ end mass_energy(p::AbstractParticle) = _format_energy(uconvert(u"eV", p.mass * Unitful.c^2)) -function Base.getproperty(p::ChargedParticleImpl, s::Symbol) +function Base.getproperty(p::AbstractParticle, s::Symbol) + s in fieldnames(typeof(p)) && return getfield(p, s) s in calculated_properties && return eval(get(properties_fn_map, s, s))(p) s in keys(synonym_properties) && return getproperty(p, synonym_properties[s]) - return getfield(p, s) end -Base.propertynames(p::ChargedParticleImpl) = (sort ∘ collect ∘ union)(keys(synonym_properties), calculated_properties, fieldnames(ChargedParticleImpl)) +function Base.propertynames(::T) where {T<:AbstractParticle} + (sort ∘ collect ∘ union)(keys(synonym_properties), calculated_properties, fieldnames(T)) +end diff --git a/src/special_particles.jl b/src/special_particles.jl new file mode 100644 index 0000000..78db671 --- /dev/null +++ b/src/special_particles.jl @@ -0,0 +1,38 @@ +const leptons = ("e-", "mu-", "tau-", "nu_e", "nu_mu", "nu_tau") +const antileptons = ("e+", "mu+", "tau+", "anti_nu_e", "anti_nu_mu", "anti_nu_tau") +const baryons = ("p+", "n") +const antibaryons = ("p-", "antineutron") + +struct Electron <: AbstractLepton end +struct Positron <: AbstractLepton end +struct Muon <: AbstractLepton end +struct Neutron <: AbstractFermion end + +# Properties +atomic_number(::AbstractFermion) = 0 +mass_number(::AbstractFermion) = 0 + +## Electron and Positron +charge_number(::Electron) = -1 +mass(::Electron) = me +symbol(::Electron) = :e + +charge_number(::Positron) = 1 +mass(::Positron) = me +symbol(::Positron) = :e + +## Muon +charge_number(::Muon) = -1 +mass(::Muon) = 206.7682827me +symbol(::Muon) = :μ + +## Neutron +charge_number(::Neutron) = 0 +mass(::Neutron) = Unitful.mn +symbol(::Neutron) = :n +mass_number(::Neutron) = 1 + + +# Convenience constructors for common particles +"""Create an electron""" +electron() = Electron() \ No newline at end of file diff --git a/src/types.jl b/src/types.jl index 663d51c..ae50846 100644 --- a/src/types.jl +++ b/src/types.jl @@ -2,99 +2,74 @@ AbstractParticle Abstract type representing any particle in plasma physics. - -See also: [`ChargedParticleImpl`](@ref) """ abstract type AbstractParticle end -# Type for particle-like inputs -const ParticleLike = Union{AbstractParticle,Symbol,AbstractString} +""" + AbstractChargeParticle <: AbstractParticle +Abstract type representing any particle that could carry an electric charge. """ - ChargedParticleImpl <: AbstractParticle +abstract type AbstractChargeParticle <: AbstractParticle end -Implementation type for charged particles. +""" + AbstractFermion <: AbstractParticle -# Fields -- `symbol::Symbol`: Chemical symbol or particle identifier (e.g., :Fe, :e, :μ) -- `charge_number::Int`: Number of elementary charges (can be negative) -- `mass_number::Int`: Total number of nucleons (protons + neutrons) +Abstract type representing fermions (particles with half-integer spin). +""" +abstract type AbstractFermion <: AbstractParticle end -# Notes -- Mass number : For elementary particles like electrons and muons, `mass_number` is 0 -- Charge number : electrical charge in units of the elementary charge, usually denoted as z. https://en.wikipedia.org/wiki/Charge_number """ -@kwdef struct ChargedParticleImpl <: AbstractParticle - symbol::Symbol - charge_number::Int - mass_number::Int -end + AbstractLepton <: AbstractFermion +Abstract type representing leptons (electron, muon, tau and their neutrinos). """ - Particle(str::AbstractString; mass_numb=nothing, z=nothing) +abstract type AbstractLepton <: AbstractFermion end -Create a particle from a string representation. +""" + AbstractQuark <: AbstractFermion -# Arguments -- `str::AbstractString`: String representation of the particle +Abstract type representing quarks (up, down, charm, strange, top, bottom). +""" +abstract type AbstractQuark <: AbstractFermion end -# String Format Support -- Element symbols: `"Fe"`, `"He"` -- Isotopes: `"Fe-56"`, `"D"` -- Ions: `"Fe2+"`, `"H-"` -- Common aliases: `"electron"`, `"proton"`, `"alpha"`, `"mu-"` +# Type for particle-like inputs +const ParticleLike = Union{AbstractParticle,Symbol,AbstractString} -# Examples -```jldoctest; output = false -# Elementary particles -electron = Particle("e-") -muon = Particle("mu-") -positron = Particle("e+") - -# Ions and isotopes -proton = Particle("H+") -alpha = Particle("He2+") -deuteron = Particle("D+") -iron56 = Particle("Fe-56") -# output -Fe -``` """ -function Particle(str::AbstractString; mass_numb=nothing, z=nothing) - # Check aliases first - if haskey(PARTICLE_ALIASES, str) - symbol, charge, mass_number = PARTICLE_ALIASES[str] - return ChargedParticleImpl(Symbol(symbol), charge, mass_number) - end + Particle <: AbstractChargeParticle - # Try to parse as element with optional mass number and charge - result = parse_particle_string(str) - if !isnothing(result) - (symbol, parsed_charge, parsed_mass_numb) = result - element = elements[symbol] - charge = determine(parsed_charge, z; default=0) - mass_number = determine(parsed_mass_numb, mass_numb; default=element.mass_number) - return ChargedParticleImpl(symbol, charge, mass_number) - end - throw(ArgumentError("Invalid particle string format: $str")) -end +Implementation type for charged particles. -""" - Particle(sym::Symbol) +# Fields +- `symbol::Symbol`: Chemical symbol or particle identifier (e.g., :Fe, :e, :μ) +- `charge_number::Int`: Number of elementary charges (can be negative) +- `mass_number::Int`: Total number of nucleons (protons + neutrons). If not provided, defaults to the most common isotope mass number -Create a particle from its symbol representation. +# Notes +- Mass number : For elementary particles like electrons and muons, `mass_number` is 0 +- Charge number : electrical charge in units of the elementary charge, usually denoted as z. https://en.wikipedia.org/wiki/Charge_number # Examples -```jldoctest; output = false -# Elementary particles -electron = Particle(:e) -muon = Particle(:muon) -proton = Particle(:p) +```jldoctest +Particle(:Fe, 2) # Creates Fe²⁺ with default mass number # output -H⁺ +Fe²⁺ ``` """ -Particle(sym::Symbol; kwargs...) = Particle(string(sym); kwargs...) +struct Particle <: AbstractChargeParticle + symbol::Symbol + charge_number::Int + mass_number::Int + function Particle(symbol, charge_number, mass_number=nothing) + if isnothing(mass_number) + mass_number = elements[symbol].mass_number + else + mass_number >= 0 || throw(ArgumentError("Mass number must be non-negative, got $mass_number")) + end + new(symbol, charge_number, mass_number) + end +end """ Particle(p::AbstractParticle) @@ -112,7 +87,7 @@ Fe-54³⁺ function Particle(p::AbstractParticle; mass_numb=nothing, z=nothing) mass_number = something(mass_numb, p.mass_number) charge_number = something(z, p.charge_number) - ChargedParticleImpl(p.symbol, charge_number, mass_number) + Particle(p.symbol, charge_number, mass_number) end """ @@ -143,11 +118,8 @@ See also: [`Particle(::AbstractString)`](@ref) function Particle(atomic_number::Int; mass_numb=nothing, z=0) element = elements[atomic_number] mass_number = something(mass_numb, element.mass_number) - ChargedParticleImpl(element.symbol, z, mass_number) + Particle(element.symbol, z, mass_number) end -# Convenience constructors for common particles -"""Create an electron""" -electron() = ChargedParticleImpl(:e, -1, 0) """Create a proton""" -proton() = ChargedParticleImpl(:p, 1, 1) \ No newline at end of file +proton() = Particle(:p, 1, 1) \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl index 5fce1a7..3ee966b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -14,7 +14,9 @@ julia> is_ion(electron()) false ``` """ -is_ion(p::AbstractParticle) = !(p.symbol in ELEMENTARY_PARTICLES) && p.charge_number != 0 +is_ion(::AbstractParticle) = false + +is_ion(p::Particle) = p.charge_number != 0 """ is_chemical_element(p::AbstractParticle) diff --git a/test/custom_particles.jl b/test/custom_particles.jl new file mode 100644 index 0000000..db47445 --- /dev/null +++ b/test/custom_particles.jl @@ -0,0 +1,15 @@ +using Test, ChargedParticles, Unitful +using Unitful: q + +@testset "CustomParticle" begin + # Test basic construction + p1 = CustomParticle(1.67e-27u"kg", 1) + @test mass(p1) == 1.67e-27u"kg" + @test p1.charge_number == 1 + @test p1.symbol == :custom + + # Test with custom symbol + p2 = CustomParticle(2.0u"GeV", -1, :my_particle) + @test mass_energy(p2) == 2.0u"GeV" + @test charge(p2) == -Unitful.q +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 2724437..24265df 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,5 @@ using Test using ChargedParticles -using ChargedParticles: is_electron, is_proton using Unitful using Unitful: q, me @@ -31,6 +30,10 @@ using Mendeleev: elements include("types.jl") end + @testset "Custom Particles" begin + include("custom_particles.jl") + end + @testset "Properties" begin include("properties.jl") end @@ -39,9 +42,6 @@ using Mendeleev: elements # Test invalid particle strings @test_throws KeyError Particle("invalid") @test_throws KeyError Particle("Xx") - - # Test invalid mass numbers - @test_throws MethodError Particle(:He, 2, -1) end @testset "String Representation" begin diff --git a/test/types.jl b/test/types.jl index 665257e..d610e73 100644 --- a/test/types.jl +++ b/test/types.jl @@ -1,6 +1,15 @@ using Test, ChargedParticles, Unitful +using ChargedParticles: is_electron, is_proton using Unitful: q +@testset "Constructor" begin + # Test invalid mass numbers + @test_throws ArgumentError Particle(:He, 2, -1) + + # Default mass number + @test mass_number(Particle(:He, 2)) == 4 +end + @testset "String Constructor" begin # Test common aliases @test is_electron(Particle("electron"))