├── .gitmodules ├── ebtelplusplus ├── data │ ├── __init__.py │ └── radiation │ │ ├── abund_10_rad_loss.dat │ │ ├── abund_15_rad_loss.dat │ │ ├── abund_20_rad_loss.dat │ │ ├── abund_25_rad_loss.dat │ │ ├── abund_30_rad_loss.dat │ │ ├── abund_35_rad_loss.dat │ │ └── abund_40_rad_loss.dat ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_rad_loss.py │ ├── test_single_fluid.py │ ├── test_compare_hydrad.py │ ├── test_compare_idl.py │ ├── test_solver.py │ └── helpers.py ├── _low_level.py ├── __init__.py ├── extern │ ├── misc.h │ ├── misc.cpp │ ├── observer.cpp │ ├── heater.cpp │ ├── observer.h │ ├── dem.h │ ├── heater.h │ ├── constants.h │ ├── ebtel.cpp │ ├── dem.cpp │ ├── helper.h │ └── loop.h ├── high_level.py └── models.py ├── examples ├── README.rst ├── electron_single_event.py ├── ion_multi_event.py ├── single_trapezoid_event.py └── area_expansion.py ├── docs ├── bibliography.rst ├── topic_guides │ ├── index.rst │ ├── comparison.rst │ └── derivation.rst ├── reference.rst ├── nitpick-exceptions ├── Makefile ├── make.bat ├── development.rst ├── index.rst ├── references.bib └── conf.py ├── .rtd-environment.yml ├── MANIFEST.in ├── .readthedocs.yml ├── .github └── workflows │ ├── scheduled_builds.yml │ ├── sub_package_update.yml │ └── ci.yml ├── CMakeLists.txt ├── .cruft.json ├── .pre-commit-config.yaml ├── .zenodo.json ├── tox.ini ├── README.md ├── CODE_OF_CONDUCT.md ├── .gitignore └── pyproject.toml /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ebtelplusplus/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ebtelplusplus/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Examples 3 | ======== 4 | -------------------------------------------------------------------------------- /docs/bibliography.rst: -------------------------------------------------------------------------------- 1 | .. _ebtelplusplus-bibliography: 2 | 3 | Bibliography 4 | ------------ 5 | 6 | .. bibliography:: 7 | -------------------------------------------------------------------------------- /ebtelplusplus/_low_level.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interface to the underlying C++ module 3 | """ 4 | from ._core import run 5 | 6 | __all__ = ["run"] 7 | -------------------------------------------------------------------------------- /.rtd-environment.yml: -------------------------------------------------------------------------------- 1 | name: rtd_ebtelplusplus 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.12 6 | - pip 7 | - graphviz!=2.42.*,!=2.43.* 8 | -------------------------------------------------------------------------------- /docs/topic_guides/index.rst: -------------------------------------------------------------------------------- 1 | .. _ebtelplusplus-topic-guide: 2 | 3 | Topic Guides 4 | ============ 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | comparison 10 | derivation 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Exclude specific files 2 | # All files which are tracked by git and not explicitly excluded here are included by setuptools_scm 3 | # Prune folders 4 | prune build 5 | prune docs/_build 6 | prune docs/api 7 | prune docs/generated 8 | global-exclude *.pyc *.o 9 | -------------------------------------------------------------------------------- /ebtelplusplus/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ebtelplusplus: Zero-dimensional hydrodynamics of coronal loops 3 | """ 4 | from ebtelplusplus.high_level import EbtelResult, run 5 | 6 | try: 7 | from ebtelplusplus._version import __version__ 8 | except ImportError: 9 | __version__ = "unknown" 10 | 11 | __all__ = ["run", "EbtelResult", "__version__"] 12 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | .. _ebtelplusplus-reference: 2 | 3 | API Reference 4 | ============= 5 | 6 | ebtelplusplus 7 | ------------- 8 | 9 | .. automodapi:: ebtelplusplus 10 | :no-heading: 11 | :no-main-docstr: 12 | :no-inheritance-diagram: 13 | 14 | ebtelplusplus.models 15 | -------------------- 16 | 17 | .. automodapi:: ebtelplusplus.models 18 | :no-heading: 19 | -------------------------------------------------------------------------------- /ebtelplusplus/extern/misc.h: -------------------------------------------------------------------------------- 1 | // **** 2 | // * 3 | // * Header file for miscellaneous functions 4 | // * 5 | // **** 6 | 7 | #ifndef MISC_H 8 | #define MISC_H 9 | 10 | #include 11 | #include 12 | 13 | // Find the closest value in a vector to the input x 14 | // Uses a simple search, O(N), so it's best for short arrays 15 | // Returns the index of the vector that's closest to value x 16 | int find_closest(double x, std::vector array, int array_length); 17 | 18 | #endif -------------------------------------------------------------------------------- /ebtelplusplus/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption( 6 | "--ebtel_idl_path", action="store", default=None, help="Path to EBTEL IDL code" 7 | ) 8 | parser.addoption( 9 | '--plot_idl_comparisons', action='store_true', default=False, help='Plot IDL results against ebtel++' 10 | ) 11 | 12 | 13 | @pytest.fixture 14 | def ebtel_idl_path(request): 15 | return request.config.getoption("--ebtel_idl_path") 16 | 17 | 18 | @pytest.fixture 19 | def plot_idl_comparisons(request): 20 | return request.config.getoption("--plot_idl_comparisons") 21 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-lts-latest 5 | tools: 6 | python: "mambaforge-latest" 7 | jobs: 8 | post_checkout: 9 | - git fetch --unshallow || true 10 | pre_install: 11 | - git update-index --assume-unchanged .rtd-environment.yml docs/conf.py 12 | apt_packages: 13 | - libboost-all-dev 14 | 15 | sphinx: 16 | builder: html 17 | configuration: docs/conf.py 18 | fail_on_warning: false 19 | 20 | conda: 21 | environment: .rtd-environment.yml 22 | 23 | formats: 24 | - htmlzip 25 | 26 | python: 27 | install: 28 | - method: pip 29 | path: . 30 | extra_requirements: 31 | - docs 32 | - all 33 | -------------------------------------------------------------------------------- /ebtelplusplus/extern/misc.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | misc.cpp 3 | Miscellaneous functions for manipulating data 4 | */ 5 | 6 | #include "misc.h" 7 | 8 | int find_closest(double x, std::vector array, int array_length) 9 | { 10 | /* Traverses the whole array, so O(N) search. 11 | * A binary search would be more efficient for long arrays: O(ln N). */ 12 | int index = 0; 13 | double closest_value = array[0]; 14 | 15 | for( int i=1; i < array_length; ++i ) 16 | { 17 | if( std::abs(x - closest_value) > std::abs(x - array[i]) ) 18 | { 19 | closest_value = array[i]; 20 | index = i; 21 | } 22 | } 23 | 24 | return index; 25 | } -------------------------------------------------------------------------------- /.github/workflows/scheduled_builds.yml: -------------------------------------------------------------------------------- 1 | name: Scheduled builds 2 | 3 | on: 4 | # Allow manual runs through the web UI 5 | workflow_dispatch: 6 | schedule: 7 | # ┌───────── minute (0 - 59) 8 | # │ ┌───────── hour (0 - 23) 9 | # │ │ ┌───────── day of the month (1 - 31) 10 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 11 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 12 | - cron: '0 12 * * 1' # Every Mon at 12:00 UTC 13 | 14 | jobs: 15 | dispatch_workflows: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - run: gh workflow run ci.yml --repo rice-solar-physics/ebtelplusplus --ref main 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /docs/nitpick-exceptions: -------------------------------------------------------------------------------- 1 | # Prevents sphinx nitpicky mode picking up on optional 2 | # (see https://github.com/sphinx-doc/sphinx/issues/6861) 3 | # Even if it was "fixed", still broken 4 | py:class optional 5 | # See https://github.com/numpy/numpy/issues/10039 6 | py:obj numpy.datetime64 7 | # There's no specific file or function classes to link to 8 | py:class any type 9 | py:class array-like 10 | py:class file object 11 | py:class function 12 | py:class path-like 13 | py:class str-like 14 | py:class time-like 15 | py:obj function 16 | py:obj iterable 17 | # Units 18 | py:class Unit 19 | py:class Unit('1 / cm3') 20 | py:class Unit('erg') 21 | py:class Unit('Angstrom') 22 | py:class Unit('1 / cm5') 23 | py:class Unit('K') 24 | py:class dimensionless 25 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15...3.26) 2 | project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX) 3 | 4 | set(CMAKE_CXX_STANDARD 17 CACHE STRING "C++ version selection") 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | # Force macOS toolchains to use C++17 8 | set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15) 9 | 10 | set(PYBIND11_FINDPYTHON ON) 11 | find_package(pybind11 CONFIG REQUIRED) 12 | 13 | set(Boost_DEBUG ON) 14 | find_package(Boost 1.53 REQUIRED) 15 | if(Boost_FOUND) 16 | include_directories(${Boost_INCLUDE_DIRS}) 17 | endif() 18 | 19 | file(GLOB SRC_FILES ${SKBUILD_PROJECT_NAME}/extern/*.cpp) 20 | file(GLOB HEADER_FILES ${SKBUILD_PROJECT_NAME}/extern/*.h) 21 | 22 | pybind11_add_module(_core MODULE ${SRC_FILES} ${HEADER_FILES}) 23 | install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME}) 24 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | clean: 23 | rm -rf $(BUILDDIR) 24 | rm -rf ./generated 25 | rm -rf ./api 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /ebtelplusplus/tests/test_rad_loss.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the different kinds of radiative loss functions 3 | """ 4 | import astropy.units as u 5 | import pytest 6 | 7 | import ebtelplusplus 8 | 9 | from ebtelplusplus.models import HeatingModel, PhysicsModel, TriangularHeatingEvent 10 | 11 | 12 | @pytest.mark.parametrize('radiation', ['power_law', 'variable', 'coronal', 'photospheric']) 13 | def test_rad_loss_options(radiation): 14 | # Just a smoke test to make sure the new radiative loss options work 15 | physics = PhysicsModel(radiative_loss=radiation) 16 | heating = HeatingModel( 17 | background=1e-6*u.Unit('erg cm-3 s-1'), 18 | events=[TriangularHeatingEvent(0.0*u.s, 200*u.s, 0.1*u.Unit('erg cm-3 s-1'))] 19 | ) 20 | results = ebtelplusplus.run(5e3*u.s, 40*u.Mm, heating=heating, physics=physics) 21 | quantities = [ 22 | 'electron_temperature', 23 | 'ion_temperature', 24 | 'density', 25 | 'electron_pressure', 26 | 'ion_pressure', 27 | 'velocity' 28 | ] 29 | for q in quantities: 30 | assert getattr(results, q) is not None 31 | -------------------------------------------------------------------------------- /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/OpenAstronomy/packaging-guide", 3 | "commit": "ad6da17e1d8fd96456566abed30b510bf8b5f8c8", 4 | "checkout": null, 5 | "context": { 6 | "cookiecutter": { 7 | "package_name": "ebtelplusplus", 8 | "module_name": "ebtelplusplus", 9 | "short_description": "Zero-dimensional hydrodynamics of coronal loops", 10 | "author_name": "Will Barnes", 11 | "author_email": "will.t.barnes@gmail.com", 12 | "project_url": "https://github.com/rice-solar-physics/ebtelplusplus", 13 | "license": "GNU GPL v3+", 14 | "minimum_python_version": "3.10", 15 | "use_compiled_extensions": "y", 16 | "enable_dynamic_dev_versions": "y", 17 | "include_example_code": "n", 18 | "include_cruft_update_github_workflow": "y", 19 | "_sphinx_theme": "alabaster", 20 | "_parent_project": "", 21 | "_install_requires": "", 22 | "_copy_without_render": [ 23 | "docs/_templates", 24 | "docs/_static", 25 | ".github/workflows/sub_package_update.yml" 26 | ], 27 | "_template": "https://github.com/OpenAstronomy/packaging-guide" 28 | } 29 | }, 30 | "directory": null 31 | } 32 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: "v0.4.4" 4 | hooks: 5 | - id: ruff 6 | args: ["--fix"] 7 | 8 | - repo: https://github.com/PyCQA/isort 9 | rev: 5.13.2 10 | hooks: 11 | - id: isort 12 | name: isort 13 | entry: isort 14 | require_serial: true 15 | language: python 16 | types: 17 | - python 18 | 19 | - repo: https://github.com/pre-commit/pre-commit-hooks 20 | rev: v4.6.0 21 | hooks: 22 | - id: check-ast 23 | - id: check-case-conflict 24 | - id: trailing-whitespace 25 | exclude: ".*(.fits|.fts|.fit|.txt|.pro|.asdf|.json|.cpp|.h)$" 26 | - id: check-yaml 27 | - id: debug-statements 28 | - id: check-added-large-files 29 | - id: end-of-file-fixer 30 | exclude: ".*(.fits|.fts|.fit|.txt|.pro|.asdf|.json|.bib|tca.*|.cpp|.h)$" 31 | - id: mixed-line-ending 32 | exclude: ".*(.fits|.fts|.fit|.txt|.bib|.pro|.asdf|.json|tca.*|.cpp|.h)$" 33 | 34 | - repo: https://github.com/codespell-project/codespell 35 | rev: v2.2.6 36 | hooks: 37 | - id: codespell 38 | additional_dependencies: 39 | - tomli 40 | -------------------------------------------------------------------------------- /ebtelplusplus/tests/test_single_fluid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test whether single-fluid option keeps electron and ion temperatures equal 3 | """ 4 | import astropy.units as u 5 | import pytest 6 | 7 | import ebtelplusplus 8 | 9 | from ebtelplusplus.models import HeatingEvent, HeatingModel, PhysicsModel 10 | 11 | 12 | @pytest.fixture() 13 | def physics_model(): 14 | return PhysicsModel(force_single_fluid=True, 15 | saturation_limit=1/6, 16 | c1_conduction=2.0,) 17 | 18 | 19 | @pytest.mark.parametrize('event', [ 20 | HeatingEvent(0*u.s, 2000*u.s, 250*u.s, 1000*u.s, 0.005*u.Unit('erg cm-3 s-1')), # gentle case 21 | HeatingEvent(0*u.s, 200*u.s, 100*u.s, 100*u.s, 0.1*u.Unit('erg cm-3 s-1')), # more impulsive case 22 | ]) 23 | def test_single_fluid_gentle(physics_model, event): 24 | heating = HeatingModel(background=1e-6*u.Unit('erg cm-3 s-1'), 25 | partition=0.5, 26 | events=[event]) 27 | results = ebtelplusplus.run(5e3*u.s, 40*u.Mm, heating=heating, physics=physics_model) 28 | assert u.allclose(results.electron_temperature, results.ion_temperature, rtol=1e-10) 29 | assert u.allclose(results.electron_pressure, results.ion_pressure, rtol=1e-10) 30 | -------------------------------------------------------------------------------- /ebtelplusplus/extern/observer.cpp: -------------------------------------------------------------------------------- 1 | /* observer.cpp 2 | Function definitions for Observer methods 3 | */ 4 | 5 | #include "observer.h" 6 | 7 | int Observer::i; 8 | LOOP Observer::loop; 9 | DEM Observer::dem; 10 | 11 | Observer::Observer(LOOP loop_object,DEM dem_object) 12 | { 13 | // Initialize counter 14 | i = 0; 15 | // Set needed objects 16 | loop = loop_object; 17 | dem = dem_object; 18 | } 19 | 20 | Observer::~Observer(void) 21 | { 22 | // Destroy things here 23 | } 24 | 25 | void Observer::Observe(const state_type &state, const double time) 26 | { 27 | // Store state 28 | loop->SetState(state); 29 | // Save terms 30 | loop->SaveTerms(); 31 | // Calculate DEM 32 | if(loop->parameters.calculate_dem) 33 | { 34 | dem->CalculateDEM(i); 35 | } 36 | // Save results 37 | loop->SaveResults(time); 38 | // Increment counter 39 | i++; 40 | } 41 | 42 | int Observer::CheckNan(state_type &state, double &time, double &tau, double old_time, double old_tau) 43 | { 44 | // Check for NaNs in the state 45 | for(int j=0; jparameters.adaptive_solver_safety; 52 | state = loop->GetState(); 53 | return 1; 54 | } 55 | } 56 | 57 | // Pass otherwise 58 | return 0; 59 | } 60 | 61 | int Observer::CheckNan(state_type &state) 62 | { 63 | // Check for NaNs in the state 64 | for(int j=0; j(); 11 | partition = heating_config["partition"].cast(); 12 | for(auto event : heating_config["events"]) 13 | { 14 | time_start_rise.push_back(event["rise_start"].cast()); 15 | time_end_rise.push_back(event["rise_end"].cast()); 16 | time_start_decay.push_back(event["decay_start"].cast()); 17 | time_end_decay.push_back(event["decay_end"].cast()); 18 | rate.push_back(event["rate"].cast()); 19 | } 20 | num_events = rate.size(); 21 | } 22 | 23 | Heater::Heater(void) 24 | { 25 | // Default constructor 26 | } 27 | 28 | Heater::~Heater(void) 29 | { 30 | //Destructor--free some stuff here 31 | } 32 | 33 | double Heater::Get_Heating(double time) 34 | { 35 | double heat = background; 36 | for(int i=0;i= time_start_rise[i] && time < time_end_rise[i]) 39 | { 40 | heat += rate[i]*(time - time_start_rise[i])/(time_end_rise[i] - time_start_rise[i]); 41 | } 42 | else if(time >= time_end_rise[i] && time < time_start_decay[i]) 43 | { 44 | heat += rate[i]; 45 | } 46 | else if(time >= time_start_decay[i] && time < time_end_decay[i]) 47 | { 48 | heat += rate[i]*(time_end_decay[i] - time)/(time_end_decay[i] - time_start_decay[i]); 49 | } 50 | } 51 | 52 | return heat; 53 | } 54 | 55 | double Heater::Get_Time_To_Next_Heating_Change(double time) 56 | { 57 | double tau = std::numeric_limits::max(); 58 | for(int i=0;i object, used for calling save method */ 23 | static LOOP loop; 24 | /* object, used for calling save method */ 25 | static DEM dem; 26 | public: 27 | // Default constructor 28 | // @loop instance used for saving loop results 29 | // @dem instance used for saving emission measure results 30 | // 31 | // Class for monitoring the integration routine. This object includes methods 32 | // for watching the integration and saving any needed parameters at each timestep. 33 | // 34 | Observer(LOOP loop,DEM dem); 35 | 36 | // Destructor 37 | ~Observer(void); 38 | 39 | // Observer for the integrator 40 | // @state current state of the loop system 41 | // @time current time 42 | // 43 | // Method called at each step in the integration. It calls methods 44 | // from and to save relevant results. 45 | // 46 | static void Observe(const state_type &state, const double time); 47 | 48 | // Check result for NaNs 49 | // @state current state of the loop system 50 | // @time current time 51 | // @tau current timestep 52 | // @old_time previous time 53 | // @old_tau previous timestep 54 | // 55 | // Boost integrator does not check for NaNs so this is done manually. If a 56 | // NaN is found anywhere in the state vector, the state and time are set 57 | // back to the previous step and the timestep is reduced. 58 | // The overloaded function, for use with the static time step solver, only checks for NaNs 59 | // and does not reset the state or time. 60 | int CheckNan(state_type &state, double &time, double &tau, double old_time, double old_tau); 61 | int CheckNan(state_type &state); 62 | }; 63 | // Pointer to the class 64 | typedef Observer* OBSERVER; 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4.0 3 | envlist = 4 | py{311,312,313}-test 5 | py11-test-oldestdeps 6 | build_docs{,-gallery} 7 | 8 | [testenv] 9 | # tox environments are constructed with so-called 'factors' (or terms) 10 | # separated by hyphens, e.g. test-devdeps-cov. Lines below starting with factor: 11 | # will only take effect if that factor is included in the environment name. To 12 | # see a list of example environments that can be run, along with a description, 13 | # run: 14 | # 15 | # tox -l -v 16 | # 17 | description = 18 | run tests 19 | oldestdeps: with the oldest supported version of key dependencies 20 | # Pass through the following environment variables which may be needed for the CI 21 | pass_env = 22 | # A variable to tell tests we are on a CI system 23 | CI 24 | # Custom compiler locations (such as ccache) 25 | CC 26 | # Location of locales (needed by sphinx on some systems) 27 | LOCALE_ARCHIVE 28 | # If the user has set a LC override we should follow it 29 | # (note LANG is automatically passed through by tox) 30 | LC_ALL 31 | # Suppress display of matplotlib plots generated during docs build 32 | set_env = 33 | MPLBACKEND = agg 34 | # Run the tests in a temporary directory to make sure that we don't import 35 | # the package from the source tree 36 | changedir = .tmp/{envname} 37 | deps = 38 | oldestdeps: minimum_dependencies 39 | pytest-cov 40 | # The following indicates which extras_require from setup.cfg will be installed 41 | extras = 42 | test 43 | commands_pre = 44 | oldestdeps: minimum_dependencies ebtelplusplus --filename requirements-min.txt 45 | oldestdeps: pip install -r requirements-min.txt 46 | pip freeze 47 | commands = 48 | pytest --pyargs ebtelplusplus --cov ebtelplusplus --cov-report xml:coverage.xml --cov-report term-missing {posargs} 49 | 50 | [testenv:build_docs{,-gallery}] 51 | changedir = docs 52 | description = invoke sphinx-build to build the HTML docs 53 | extras = docs 54 | commands = 55 | pip freeze --all --no-input 56 | sphinx-build \ 57 | --color \ 58 | -W \ 59 | --keep-going \ 60 | -b html \ 61 | -d _build/.doctrees \ 62 | . \ 63 | _build/html \ 64 | gallery: -D plot_gallery=1 \ 65 | !gallery: -D plot_gallery=0 \ 66 | {posargs} 67 | python -c 'import pathlib; print("Documentation available under file://\{0\}".format(pathlib.Path(r"{toxinidir}") / "docs" / "_build" / "index.html"))' 68 | -------------------------------------------------------------------------------- /ebtelplusplus/extern/dem.h: -------------------------------------------------------------------------------- 1 | /* 2 | dem.h 3 | Class definition for DEM object 4 | */ 5 | 6 | #ifndef DEM_H 7 | #define DEM_H 8 | 9 | #include "helper.h" 10 | #include "loop.h" 11 | #include "constants.h" 12 | 13 | // DEM object 14 | // 15 | // Class for holding all of the methods needed to calculate 16 | // the differential emission measure in the transition region 17 | // and the corona. Requires the object for knowledge about 18 | // the evolution of the coronal loop. 19 | // 20 | class Dem{ 21 | private: 22 | // Calculate TR DEM 23 | // 24 | double CalculateDEMTR(int j,double density,double velocity,double pressure,double scale_height,double R_tr,double f_e); 25 | 26 | public: 27 | /* Loop object */ 28 | LOOP loop; 29 | 30 | /* Method option for DEM TR calculation */ 31 | bool use_new_method; 32 | 33 | /* Temperature range */ 34 | std::vector __temperature; 35 | 36 | /* Radiative loss */ 37 | std::vector __radiative_loss; 38 | 39 | /*Transition region DEM*/ 40 | std::vector > dem_TR; 41 | 42 | /*Coronal DEM*/ 43 | std::vector > dem_corona; 44 | 45 | // Default constructor 46 | // 47 | // Used when we don't want to actually do the DEM 48 | // calculation. Just a placeholder. 49 | // 50 | Dem(void); 51 | 52 | // Constructor 53 | // @loop object that provides needed parameters and methods 54 | // 55 | // Setup Dem object to calculate differential emission measure in both the 56 | // transition region and the corona. 57 | // 58 | Dem(LOOP loop); 59 | 60 | // Destructor 61 | // 62 | ~Dem(void); 63 | 64 | // Calculate DEM 65 | // @i Timestep index 66 | // 67 | // Front end for DEM calculations. Calls methods to calculate both 68 | // the transition region and coronal DEM across the entire specified 69 | // temperature range. 70 | // 71 | void CalculateDEM(int i); 72 | 73 | // Print results to file 74 | // @num_steps number of steps taken by the integration routine 75 | // 76 | // Print coronal and transition region DEM arrays to separate files. 77 | // The filenames are the output filename as given in , 78 | // suffixed by `.dem_corona` and `.dem_tr`, respectively. The first 79 | // row of each file is the temperature vector, <__temperature>. 80 | // 81 | py::dict GetFinalResults(py::dict results, int num_steps); 82 | 83 | }; 84 | // Pointer to the class 85 | typedef Dem* DEM; 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ebtelplusplus 2 | 3 | [![CI Status](https://github.com/rice-solar-physics/ebtelplusplus/actions/workflows/ci.yml/badge.svg)](https://github.com/rice-solar-physics/ebtelPlusPlus/actions/workflows/ci.yml) 4 | [![Documentation Status](https://readthedocs.org/projects/ebtelplusplus/badge/?version=latest)](https://ebtelplusplus.readthedocs.io/en/latest/?badge=latest) 5 | [![codecov](https://codecov.io/gh/rice-solar-physics/ebtelplusplus/branch/main/graph/badge.svg?token=8G5H9T5AAH)](https://codecov.io/gh/rice-solar-physics/ebtelplusplus) 6 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.12675386.svg)](https://doi.org/10.5281/zenodo.12675386) 7 | [![PyPI](https://img.shields.io/pypi/v/ebtelplusplus.svg)](https://pypi.org/project/ebtelplusplus) 8 | 9 | `ebtelplusplus` is an implementation of the enthalpy-based thermal evolution of loops (EBTEL) model for doing 10 | efficient hydrodynamics of dynamically-heated solar coronal loops. 11 | `ebtelplusplus` decouples the electron and ion energy equations such that the two populations can evolve separately. 12 | This implementation also includes effects to due to cross-sectional area expansion. 13 | 14 | If you are looking for the original EBTEL implementation, the you can find the [repository for the IDL code here](https://github.com/rice-solar-physics/EBTEL). 15 | 16 | ## Installation 17 | 18 | The easiest way to install `ebtelplusplus` is through `pip`, 19 | 20 | ```shell 21 | pip install ebtelplusplus 22 | ``` 23 | 24 | If you would like to compile and build the package from source, see [the instructions here](https://ebtelplusplus.readthedocs.org/en/stable/development.html). 25 | 26 | ## Usage 27 | 28 | The code snippet below shows how to set up a simulation for a 40 Mm loop, lasting 2 hours, heated by a single 29 | heating event lasting 200 s in which all of the energy is injected into the electrons, 30 | 31 | ```python 32 | import astropy.units as u 33 | import ebtelplusplus 34 | from ebtelplusplus.models import HeatingModel, TriangularHeatingEvent 35 | 36 | heating = HeatingModel( 37 | background=1e-6*u.Unit('erg cm-3 s-1'), 38 | partition=1, 39 | events=[TriangularHeatingEvent(0*u.s, 200*u.s, 0.1*u.Unit('erg cm-3 s-1'))] 40 | ) 41 | results = ebtelplusplus.run(2*u.h, 40*u.Mm, heating=heating) 42 | ``` 43 | 44 | ## Citation 45 | 46 | If you use `ebtelplusplus` in any published work, it is greatly appreciated if you follow the [citation instructions here](https://ebtelplusplus.readthedocs.io/en/stable/index.html#citation). 47 | -------------------------------------------------------------------------------- /ebtelplusplus/tests/test_compare_hydrad.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compare output of EBTEL IDL and ebtel++ 3 | """ 4 | import astropy.units as u 5 | import pytest 6 | 7 | from scipy.interpolate import interp1d 8 | 9 | import ebtelplusplus 10 | 11 | from ebtelplusplus.models import ( 12 | HeatingModel, 13 | PhysicsModel, 14 | SolverModel, 15 | TriangularHeatingEvent, 16 | ) 17 | 18 | from .helpers import read_hydrad_test_data 19 | 20 | 21 | @pytest.fixture() 22 | def solver_model(): 23 | return SolverModel(tau=0.1*u.s, 24 | tau_max=10*u.s, 25 | adaptive_solver_error=1e-8) 26 | 27 | 28 | @pytest.mark.parametrize('tau', [200.0*u.s, 500.0*u.s]) 29 | @pytest.mark.parametrize('heating_type', ['single', 'electron', 'ion']) 30 | def test_compare_hydrad_single_event_peak_values(tau, heating_type, solver_model): 31 | if heating_type == 'single': 32 | force_single_fluid = True 33 | partition = 0.5 34 | elif heating_type == 'electron': 35 | force_single_fluid = False 36 | partition = 1.0 37 | elif heating_type == 'ion': 38 | force_single_fluid = False 39 | partition = 0.0 40 | total_energy = 10.0 * u.erg / u.cm**3 41 | heating_model = HeatingModel( 42 | background=3.5e-5*u.Unit('erg cm-3 s-1'), 43 | partition=partition, 44 | events=[TriangularHeatingEvent(0*u.s, tau, 2 * total_energy/tau)] 45 | ) 46 | physics_model = PhysicsModel(saturation_limit=1, 47 | force_single_fluid=force_single_fluid) 48 | r_ebtel = ebtelplusplus.run(5e3*u.s, 49 | 40*u.Mm, 50 | heating=heating_model, 51 | physics=physics_model, 52 | solver=solver_model,) 53 | r_hydrad = read_hydrad_test_data(tau.to_value('s'), heating_type) 54 | # Require all quantities at the peak to be <20% in accordance with the comparisons 55 | # done in Barnes et al. (2016a) 56 | for name in ['electron_temperature', 'ion_temperature', 'density']: 57 | f_interp = interp1d(r_ebtel.time.to_value('s'), 58 | getattr(r_ebtel, name).to_value(r_hydrad[name].unit), 59 | fill_value='extrapolate') 60 | x_ebtel_interp = u.Quantity(f_interp(r_hydrad['time'].to_value('s')), r_hydrad[name].unit) 61 | i_peak = r_hydrad[name].argmax() 62 | assert u.allclose(x_ebtel_interp[i_peak], r_hydrad[name][i_peak], rtol=0.20) 63 | -------------------------------------------------------------------------------- /ebtelplusplus/extern/heater.h: -------------------------------------------------------------------------------- 1 | /* 2 | heater.h 3 | Class definition for the heating object 4 | */ 5 | 6 | #ifndef HEATER_H 7 | #define HEATER_H 8 | 9 | #include "helper.h" 10 | #include "constants.h" 11 | 12 | // Heater object 13 | // 14 | // Class for configuring time-dependent heating profiles. 15 | // Accepts a properly formatted XML node and 16 | // calculates the heating rate at any time. Heating profiles 17 | // must be specified in terms of heating pulses 18 | // plus a static background . You can also initialize a blank 19 | // object and set the event parameters later on. 20 | // 21 | class Heater { 22 | 23 | public: 24 | 25 | /*Background heating rate (in erg cm^-3 s^-1) */ 26 | double background; 27 | 28 | /* Number of events */ 29 | int num_events; 30 | 31 | /*Starting time of the rise phase (in s) */ 32 | std::vector time_start_rise; 33 | 34 | /*Ending time of the rise phase (in s) */ 35 | std::vector time_end_rise; 36 | 37 | /*Starting time of the decay phase (in s) */ 38 | std::vector time_start_decay; 39 | 40 | /*Ending time of the decay phase (in s) */ 41 | std::vector time_end_decay; 42 | 43 | /*Heating rates of the events (in erg cm^-3 s^-1) */ 44 | std::vector rate; 45 | 46 | /* Partition of energy between electrons and ions; 1 corresponds to pure electron heating and 0 pure ion heating. For a single-fluid treatment, use 0.5 */ 47 | double partition; 48 | 49 | // Constructor 50 | // @heating_node XML node holding the heating information 51 | // 52 | Heater(py::dict heating_config); 53 | 54 | // Default constructor 55 | // 56 | Heater(void); 57 | 58 | /* Destructor */ 59 | ~Heater(void); 60 | 61 | // Get heating at time