Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
348d7f1
Implementation of the rates matrix and level pop solvers into plasma
andrewfullard Dec 3, 2024
1f8d852
Demonstration notebook
andrewfullard Dec 3, 2024
1829a04
Vague ideas
andrewfullard Jan 22, 2025
bdfe800
Functional ion population solver
andrewfullard Feb 5, 2025
0cecf6e
Add corrected coefficient solver
andrewfullard Feb 17, 2025
9c61d53
Merge branch 'master' into photoion-rates-implementation
andrewfullard Mar 31, 2025
1972152
Correct multiplication to compute strengths
andrewfullard Apr 8, 2025
9b691f2
Simplify and fix various parts of the rate construction
andrewfullard Apr 8, 2025
e7da551
Update rate matrix construction
andrewfullard Apr 16, 2025
b9bd98e
Add recombination rates to matrix
andrewfullard Apr 16, 2025
1da22ba
Handle charge conservation and matrix solving
andrewfullard Apr 22, 2025
6251a6c
Add collisional rates to the matrix
andrewfullard Apr 22, 2025
2f26e34
Correct recombination rate calculation
andrewfullard Apr 25, 2025
4dba751
Iterative ion population solver
andrewfullard Apr 29, 2025
69f3b48
Formatting
andrewfullard May 5, 2025
85dc58e
Collisional rate index fix, ion solver iteration value
andrewfullard May 6, 2025
49e6edf
Add demo notebook
andrewfullard May 6, 2025
581371f
Basic collisional ionization strength tests
andrewfullard May 6, 2025
408eb53
Improve estimator-based rate calculation
andrewfullard May 6, 2025
94a039a
Very basic tests for collisional ionization rate class
andrewfullard May 6, 2025
b2bc887
Hydrogen continuum property
andrewfullard May 13, 2025
d43cb05
Output electron densities from the continuum solver
andrewfullard May 13, 2025
bda79ae
Merge branch 'master' into photoion-rates-implementation
andrewfullard Jun 2, 2025
ab29b5c
Some basic (failing) tests
andrewfullard Jun 2, 2025
39fb72c
Hydrogen plasma additions
andrewfullard Jun 2, 2025
bdb62cf
Remove unnecessary lambda
andrewfullard Jun 2, 2025
6847b1d
Batch of test fixes
andrewfullard Jun 2, 2025
76d3ffd
Further test fixes
andrewfullard Jun 2, 2025
8ac3c3b
Fix the rest of the tests
andrewfullard Jun 3, 2025
21a07ac
Fix tests and improve/correct docstrings
andrewfullard Jun 3, 2025
c99bbdd
Update notebook
andrewfullard Jun 20, 2025
daa2a5b
Factor out reindexing utility
andrewfullard Jun 24, 2025
c0b45bc
Response to comments
andrewfullard Jun 24, 2025
e9f8c48
Replace DiluteBlackBodyContinuumPropertiesSolver with AnalyticPhotoio…
andrewfullard Jun 24, 2025
a5e752d
Correct property ordering
andrewfullard Jun 24, 2025
d47c7c7
Clarify population solution cuts
andrewfullard Jun 24, 2025
9cef721
Remove unnecessary call
andrewfullard Jun 24, 2025
9f029cc
Respond to comments
andrewfullard Jun 25, 2025
1c61713
Major corrections to rate matrix construction and population solver
andrewfullard Jul 2, 2025
125026b
Improvements to rate calculations
andrewfullard Jul 7, 2025
3393573
Comparison notebook
andrewfullard Jul 9, 2025
3332750
Add correct comparison notebook
andrewfullard Jul 14, 2025
102c1b6
Fix calculation of level population ratio, handle Lyman continuum for…
andrewfullard Jul 14, 2025
7cf6da3
Remove incorrect comparison notebook
andrewfullard Jul 14, 2025
e34efb9
Update tests to include boltzmann factor
andrewfullard Jul 14, 2025
f5eff36
Move solver tolerance to kwarg
andrewfullard Jul 15, 2025
8cf1276
Store comparison plots
andrewfullard Jul 18, 2025
5cc737e
Add some notes about iteration dependent quantities
andrewfullard Aug 11, 2025
7de8bcc
Finalize ion population tests
andrewfullard Aug 11, 2025
1b2c51a
Address Jack's comments
andrewfullard Aug 12, 2025
fe2577a
Merge branch 'master' into photoion-rates-implementation
andrewfullard Aug 12, 2025
7e792f1
Cleanup, renames, comments with equations
andrewfullard Aug 12, 2025
a2ccdac
Fix excess unit conversion
andrewfullard Aug 12, 2025
4cef991
Use fancy new sync dataframe
andrewfullard Aug 12, 2025
8934011
Revert "Use fancy new sync dataframe"
andrewfullard Aug 12, 2025
fc3cdf5
Lock in docs notebooks
andrewfullard Aug 13, 2025
677fc58
Use the real fancy new sync dataframe
andrewfullard Aug 14, 2025
4d8a4ad
Run tests
andrewfullard Aug 18, 2025
6fb6334
Fix test failure in old NLTE solver
andrewfullard Aug 18, 2025
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
1,655 changes: 1,655 additions & 0 deletions docs/physics/plasma/equilibrium/ionization_solution_comparison.ipynb

Large diffs are not rendered by default.

821 changes: 821 additions & 0 deletions docs/physics/plasma/equilibrium/ionization_solutions.ipynb

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions tardis/plasma/assembly/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

from tardis.plasma import BasePlasma
from tardis.plasma.base import PlasmaSolverSettings
from tardis.plasma.equilibrium.rates.photoionization_strengths import (
AnalyticPhotoionizationCoeffSolver,
)
from tardis.plasma.exceptions import PlasmaConfigError
from tardis.plasma.properties import (
HeliumNumericalNLTE,
Expand All @@ -25,7 +28,7 @@
)
from tardis.plasma.properties.rate_matrix_index import NLTEIndexHelper
from tardis.transport.montecarlo.estimators.continuum_radfield_properties import (
DiluteBlackBodyContinuumPropertiesSolver,
ContinuumProperties,
)
from tardis.util.base import species_string_to_tuple

Expand Down Expand Up @@ -534,13 +537,15 @@ def initialize_continuum_properties(self, dilute_planckian_radiation_field):
initial_continuum_properties : `~tardis.plasma.properties.ContinuumProperties`
The initial continuum properties of the plasma.
"""
t_electrons = dilute_planckian_radiation_field.temperature.to(u.K).value
t_electrons = dilute_planckian_radiation_field.temperature.to(u.K)

initial_continuum_solver = DiluteBlackBodyContinuumPropertiesSolver(
self.atom_data
initial_continuum_solver = AnalyticPhotoionizationCoeffSolver(
self.atom_data.photoionization_data
)
initial_continuum_properties = initial_continuum_solver.solve(
dilute_planckian_radiation_field, t_electrons
initial_continuum_properties = ContinuumProperties(
*initial_continuum_solver.solve(
dilute_planckian_radiation_field, t_electrons
)
)
return initial_continuum_properties

Expand Down
195 changes: 195 additions & 0 deletions tardis/plasma/equilibrium/ion_populations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import logging

import astropy.units as u
import numpy as np
import pandas as pd

logger = logging.getLogger(__name__)

LOWER_ION_LEVEL_H = 0


class IonPopulationSolver:
def __init__(self, rate_matrix_solver, max_solver_iterations=100):
"""Solve the normalized ion population values from the rate matrices.

Parameters
----------
rate_matrix_solver : IonRateMatrix
"""
self.rate_matrix_solver = rate_matrix_solver
self.max_solver_iterations = max_solver_iterations

def __calculate_balance_vector(
self,
elemental_number_density,
rate_matrix_index,
charge_conservation=False,
):
"""Constructs the balance vector for the NLTE ionization solver set of
equations by combining all solution vector blocks.

Parameters
----------
elemental_number_density : pandas.Series
Elemental number densities indexed by atomic number.
rate_matrix_index : pandas.MultiIndex
(atomic_number, ion_number)
charge_conservation : bool, optional
Whether to include a charge conservation row.

Returns
-------
numpy.array
Solution vector for the NLTE ionization solver.
"""
balance_array = [
np.append(
np.zeros(
(
rate_matrix_index.get_level_values("atomic_number")
== atomic_number
).sum()
),
elemental_number_density.loc[atomic_number],
)
for atomic_number in elemental_number_density.index
]

if charge_conservation:
balance_array.append(np.array([0.0]))

return np.hstack(balance_array)

def solve(
self,
radiation_field,
thermal_electron_energy_distribution,
elemental_number_density,
lte_level_population,
estimated_level_population,
lte_ion_population,
estimated_ion_population,
partition_function,
boltzmann_factor,
charge_conservation=False,
tolerance=1e-14,
):
"""Solves the normalized ion population values from the rate matrices.

Parameters
----------
radiation_field : RadiationField
A radiation field that can compute its mean intensity.
thermal_electron_energy_distribution : ThermalElectronEnergyDistribution
Electron properties.
elemental_number_density : pd.DataFrame
Elemental number density. Index is atomic number, columns are cells.
lte_level_population : pd.DataFrame
LTE level number density. Columns are cells.
estimated_level_population : pd.DataFrame
Estimated level number density. Columns are cells.
lte_ion_population : pd.DataFrame
LTE ion number density. Columns are cells.
estimated_ion_population : pd.DataFrame
Estimated ion number density. Columns are cells.
charge_conservation : bool, optional
Whether to include a charge conservation row in the rate matrix.
tolerance : float, optional
Tolerance for convergence of the ion population solver.

Returns
-------
pd.DataFrame
Normalized ion population values indexed by atomic number, ion
number and ion number. Columns are cells.
pd.DataFrame
Normalized electron fraction values. Columns are cells.
"""
# TODO: make more general indices that work for non-Hydrogen species
# this is the i level in Lucy 2003
lower_ion_level_index = (
lte_level_population.index.get_level_values("ion_number")
== LOWER_ION_LEVEL_H
)

# this is the k level in Lucy 2003
upper_ion_population_index = (
lte_ion_population.index.get_level_values("ion_number")
> LOWER_ION_LEVEL_H
)

new_electron_energy_distribution = thermal_electron_energy_distribution
Copy link
Contributor

Choose a reason for hiding this comment

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

perhaps a copy for debugging? If not useful, ignore this comment


for iteration in range(self.max_solver_iterations):
self.rates_matrices = self.rate_matrix_solver.solve(
radiation_field,
new_electron_energy_distribution,
lte_level_population.loc[lower_ion_level_index],
estimated_level_population.loc[lower_ion_level_index],
lte_ion_population.loc[upper_ion_population_index],
estimated_ion_population.loc[upper_ion_population_index],
partition_function,
boltzmann_factor,
charge_conservation,
)
solved_matrices = pd.DataFrame(
index=self.rates_matrices.index,
columns=self.rates_matrices.columns,
)
for cell in elemental_number_density.columns:
balance_vector = self.__calculate_balance_vector(
elemental_number_density[cell],
self.rates_matrices.index,
charge_conservation,
)
solved_matrix = self.rates_matrices[cell].apply(
np.linalg.solve, args=(balance_vector,)
)
solved_matrices[cell] = solved_matrix

ion_population_solution = pd.DataFrame(
np.vstack(solved_matrices.values[0]).T,
index=estimated_ion_population.index,
columns=self.rates_matrices.columns,
)

if (ion_population_solution < 0).any().any():
ion_population_solution[ion_population_solution < 0] = 0.0

electron_population_solution = (
ion_population_solution
* ion_population_solution.index.get_level_values("ion_number")
.values[np.newaxis]
.T
).sum()

delta_ion = (
estimated_ion_population - ion_population_solution
) / ion_population_solution
delta_electron = (
new_electron_energy_distribution.number_density.value
- electron_population_solution
) / electron_population_solution

if (
np.all(np.abs(delta_ion) < tolerance).any().any()
and (np.abs(delta_electron) < tolerance).any().any()
):
logger.info(
"Ion population solver converged after %d iterations.",
iteration + 1,
)
break

estimated_ion_population = ion_population_solution
new_electron_energy_distribution.number_density = (
electron_population_solution.values * u.cm**-3
)
else:
logger.warning(
"Ion population solver did not converge after %d iterations.",
iteration,
)

return ion_population_solution, electron_population_solution
6 changes: 2 additions & 4 deletions tardis/plasma/equilibrium/level_populations.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __calculate_level_population(self, rates_matrix: np.ndarray):
normalized_ion_population = np.zeros(rates_matrix.shape[0])
normalized_ion_population[0] = 1.0
normalized_level_population = np.linalg.solve(
rates_matrix[:, :], normalized_ion_population
rates_matrix, normalized_ion_population
)
return normalized_level_population

Expand All @@ -58,9 +58,7 @@ def solve(self):
# is needed

solved_matrices = self.rates_matrices.loc[species_id].apply(
lambda rates_matrix: self.__calculate_level_population(
rates_matrix
)
self.__calculate_level_population
)
normalized_level_populations.loc[species_id, :] = np.vstack(
solved_matrices.values
Expand Down
Loading
Loading