Initial MOCCA standalone import

This commit is contained in:
Michael W. Heiss
2026-04-19 19:42:30 +02:00
commit fc80b71aad
26 changed files with 8742 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
build/
cmake-build-*/
*.swp
*.swo
__pycache__/
+39
View File
@@ -0,0 +1,39 @@
cmake_minimum_required(VERSION 3.20)
project(MOCCA VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(mocca_lib
src/api.cpp
src/config_bridge.cpp
src/embedded_tables.cpp
src/json.cpp
src/kernel.cpp
src/line_codec.cpp
src/physics_engine.cpp
)
target_include_directories(mocca_lib
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_compile_options(mocca_lib
PRIVATE
-Wall
-Wextra
-Wpedantic
)
add_executable(mocca
src/cli.cpp
)
target_link_libraries(mocca PRIVATE mocca_lib)
install(TARGETS mocca mocca_lib)
+55
View File
@@ -0,0 +1,55 @@
# MOCCA
MOCCA is the standalone modern cascade application extracted from the larger refactor work.
The name stands for `MOdern Code for CAscade`.
This repository contains only:
- the modern C++ cascade implementation
- a helper that translates legacy card decks into the modern JSON schema
- the original literature and published source material kept for provenance
It intentionally does not depend on the old Python implementation or the earlier C++ refactor.
## Build
```bash
cmake -S . -B build
cmake --build build -j
```
This produces the `mocca` executable in `build/`.
## Run
```bash
./build/mocca examples/fe_stat_n15.json --output result.json
```
The executable reads a structured JSON input file and writes a structured JSON result artifact.
The bundled coefficient tables are compiled into the binary, so no external runtime data files are required.
## Translate Legacy Decks
```bash
python3 tools/card_to_json.py legacy_case.inp --output case.json
```
The translator is a standalone helper built on the Python standard library only.
It converts the historical card input into the JSON schema used by MOCCA.
## Project Layout
- `include/mocca`: public headers
- `src`: MOCCA implementation
- `tools/card_to_json.py`: legacy-card to JSON translator
- `examples`: example JSON inputs
- `docs`: high-level documentation
- `literature`: original papers and published source material
## Literature
The `literature/` folder keeps the original papers and source material that the implementation was checked against:
- `Akylas_VR_1978.pdf`
- `1-s2.0-0010465578900991-main.pdf`
- `aama_v1_0.f`
+19
View File
@@ -0,0 +1,19 @@
# Architecture
MOCCA keeps a small public surface:
- `mocca::SimulationConfig` describes one cascade run as typed JSON-backed data.
- `mocca::run_simulation()` validates the config, runs the kernel, and returns a structured result artifact.
- `mocca` is the command-line wrapper around that library API.
Internally, the code is split into a few focused layers:
- `api.cpp`: JSON parsing and artifact serialization
- `config_bridge.cpp`: conversion from the public schema into the internal kernel state
- `physics_engine.cpp`: matrix elements, transition rates, and atomic-model preparation
- `kernel.cpp`: cascade propagation and state/line collection
- `line_codec.cpp`: packed line bookkeeping and decoding
- `embedded_tables.cpp`: compiled-in coefficient tables
The modern public interface deliberately avoids the historical card vocabulary.
The old deck format survives only in the optional translator helper.
+18
View File
@@ -0,0 +1,18 @@
# JSON Schema Notes
The MOCCA input schema is organized around physics concepts rather than card names:
- `atom`: atomic number, shell charges, binding energies, and mass information
- `transitions`: explicit transition-energy inputs and optional Dirac-energy overrides
- `capture`: initial capture distribution
- `channels`: active monopole, dipole, quadrupole, and octupole channel settings
- `shell_model`: subshell populations, refill behavior, polarization handling, and shell cutoffs
- `reporting`: line filtering and output thresholds
- `model`: low-level physical constants that remain configurable
- `numerics`: numerical control knobs exposed by the standalone implementation
The output artifact mirrors that structure and adds:
- provenance fields such as `implementation_name`, `numerical_backend`, and `coefficient_table_id`
- the normalized `input`
- the computed `result`, including line catalog, state summaries, warnings, and Lyman sum
+63
View File
@@ -0,0 +1,63 @@
{
"schema_version": 1,
"metadata": {
"case_name": "fe_stat_n15"
},
"atom": {
"atomic_number": 26.0,
"effective_shell_charges": [24.0, 22.0, 17.0],
"binding_energies_ev": [7.112e3, 8.46e2, 7.6e1],
"atomic_mass": 56.0,
"exact_mass_number": null
},
"masses": {
"muon_electron_masses": 206.7686,
"electron_mass_ev": 511003.4,
"nucleon_mass_mev": 931.48
},
"transitions": {
"two_p_to_one_s_energy_ev": null,
"two_s_to_two_p_split_ev": null,
"dirac_energies": []
},
"capture": {
"mode": "statistical_l",
"n_max": 15,
"alpha": 0.22
},
"channels": {
"case_counts": [3, 4, 4, 4],
"monopole_shells": [1, 2, 3],
"dipole_shells": [0, 1, 2, 3],
"quadrupole_shells": [0, 1, 2, 3],
"octupole_shells": [0, 1, 2, 3],
"dipole_subshell_channels": [0, 0, 0, 0],
"quadrupole_subshell_channels": [0, 0, 0, 0],
"octupole_subshell_channels": [0, 0, 0, 0],
"dipole_penetration_codes": [0, 1, 1, 1],
"quadrupole_penetration_codes": [0, 1, 1, 1],
"octupole_penetration_codes": [0, 1, 1, 1],
"dipole_penetration_avg_n_cutoffs": [0, 0, 0, 0],
"quadrupole_penetration_avg_n_cutoffs": [0, 0, 0, 0],
"octupole_penetration_avg_n_cutoffs": [0, 0, 0, 0]
},
"shell_model": {
"subshell_populations": [1, 1, 1, 1, 1, 1],
"refill_codes": [0, 0, 0],
"penetration_cutoffs": [1, 1, 1, 1],
"width_k_ev": 0.0,
"track_polarization": true
},
"reporting": {
"line_energy_min_mev": 0.04,
"line_energy_max_mev": 20.0,
"line_intensity_threshold": 1.0e-6,
"energy_resolution_mev": 0.0003
},
"model": {
"factorial_divider": 15.0
},
"numerics": {
"matrix_element_precision_digits": 120
}
}
+205
View File
@@ -0,0 +1,205 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include "mocca/json.hpp"
namespace mocca {
struct DiracEnergy {
int n{};
int kappa{};
double vacuum_polarization_kev{};
double binding_kev{};
};
struct NlWeight {
int n{};
int l{};
double weight{};
};
enum class CaptureMode {
statistical_l,
quadratic_l,
explicit_l,
explicit_nl,
legacy_empty,
};
struct MetadataConfig {
std::string case_name;
};
struct AtomConfig {
double atomic_number{};
std::vector<double> effective_shell_charges;
std::vector<double> binding_energies_ev;
double atomic_mass{};
std::optional<double> exact_mass_number;
};
struct MassConfig {
double muon_electron_masses{206.7682827};
double electron_mass_ev{510998.95069};
double nucleon_mass_mev{938.272013};
};
struct TransitionConfig {
std::optional<double> two_p_to_one_s_energy_ev;
std::optional<double> two_s_to_two_p_split_ev;
std::vector<DiracEnergy> dirac_energies;
};
/**
* Initial capture distribution configuration.
*
* The modern interface names the supported distribution families explicitly
* instead of routing them through the historical `NOP`/`IP` control cards.
*/
struct CaptureConfig {
CaptureMode mode{CaptureMode::statistical_l};
int n_max{15};
double alpha{0.0};
std::vector<double> quadratic_coefficients;
std::vector<double> l_weights;
std::vector<NlWeight> nl_weights;
};
struct ChannelConfig {
std::vector<int> case_counts{3, 4, 4, 4};
std::vector<int> monopole_shells{1, 2, 3};
std::vector<int> dipole_shells{0, 1, 2, 3};
std::vector<int> quadrupole_shells{0, 1, 2, 3};
std::vector<int> octupole_shells{0, 1, 2, 3};
std::vector<int> dipole_subshell_channels{0, 0, 0, 0};
std::vector<int> quadrupole_subshell_channels{0, 0, 0, 0};
std::vector<int> octupole_subshell_channels{0, 0, 0, 0};
std::vector<int> dipole_penetration_codes{0, 1, 1, 1};
std::vector<int> quadrupole_penetration_codes{0, 1, 1, 1};
std::vector<int> octupole_penetration_codes{0, 1, 1, 1};
std::vector<int> dipole_penetration_avg_n_cutoffs{0, 0, 0, 0};
std::vector<int> quadrupole_penetration_avg_n_cutoffs{0, 0, 0, 0};
std::vector<int> octupole_penetration_avg_n_cutoffs{0, 0, 0, 0};
};
struct ShellModelConfig {
std::vector<double> subshell_populations{1.0, 1.0, 1.0, 1.0, 1.0, 1.0};
std::vector<int> refill_codes{0, 0, 0};
std::vector<double> penetration_cutoffs{1.0, 1.0, 1.0, 1.0};
double width_k_ev{0.0};
bool track_polarization{true};
};
struct ReportingConfig {
double line_energy_min_mev{0.040};
double line_energy_max_mev{20.0};
double line_intensity_threshold{1.0e-06};
double energy_resolution_mev{0.000300};
};
struct ModelConfig {
double factorial_divider{15.0};
};
struct NumericsConfig {
int matrix_element_precision_digits{120};
};
/**
* Structured input schema for the new public-facing cascade interface.
*
* The schema is deliberately aligned with the validated cascade model while
* removing the legacy punch-card interface. It exposes the physics controls
* and precision settings needed by the modern kernel without leaking the
* historical deck/card vocabulary into the public API.
*/
struct SimulationConfig {
int schema_version{1};
MetadataConfig metadata;
AtomConfig atom;
MassConfig masses;
TransitionConfig transitions;
CaptureConfig capture;
ChannelConfig channels;
ShellModelConfig shell_model;
ReportingConfig reporting;
ModelConfig model;
NumericsConfig numerics;
};
struct TransitionLine {
int n1{};
int l1{};
int j1_twice{};
int n2{};
int l2{};
int j2_twice{};
std::string multipole;
double energy_kev{};
double intensity{};
};
struct StateSummary {
int n{};
int l{};
double population{};
std::optional<double> polar_up;
std::optional<double> polar_down;
std::optional<double> width_ev;
std::optional<double> rad_to_auger;
std::optional<double> spin_orbit_ev;
double k_electrons{};
double l_electrons{};
double m_electrons{};
};
struct SimulationResult {
double lyman_sum{};
int num_lines{};
std::vector<TransitionLine> lines;
std::vector<StateSummary> states;
std::vector<std::string> warnings;
};
/**
* JSON artifact emitted by the `mocca` CLI.
*
* It includes enough provenance to make regression outputs self-contained:
* the parsed input, the selected matrix-element precision, the coefficient-table
* source, and the resulting physics observables.
*/
struct SimulationArtifact {
int schema_version{1};
std::string implementation_name{"MOCCA"};
std::string numerical_backend{"modern_kernel"};
std::string coefficient_table_id{"aama_v1_0_block_data_v1"};
int matrix_element_precision_digits{120};
SimulationConfig input;
SimulationResult result;
};
/**
* Parse a modern JSON configuration document into a typed schema object.
*/
SimulationConfig parse_config_text(std::string_view text);
/**
* Load and parse a modern JSON configuration file.
*/
SimulationConfig load_config(const std::filesystem::path& path);
JsonValue to_json(const SimulationConfig& config);
JsonValue to_json(const SimulationResult& result);
JsonValue to_json(const SimulationArtifact& artifact);
/**
* Run the modern cascade kernel on a structured config and return a fully
* serialized-ready artifact.
*/
SimulationArtifact run_simulation(const SimulationConfig& config);
} // namespace mocca
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <map>
#include <stdexcept>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace mocca {
/**
* Minimal JSON value type used by the modern configuration and result layer.
*
* The project only needs a small subset of JSON infrastructure, so this keeps
* parsing and serialization self-contained instead of adding another external
* dependency.
*/
class JsonValue {
public:
using Array = std::vector<JsonValue>;
using Object = std::map<std::string, JsonValue>;
JsonValue() = default;
JsonValue(std::nullptr_t);
JsonValue(bool value);
JsonValue(double value);
JsonValue(int value);
JsonValue(std::string value);
JsonValue(const char* value);
JsonValue(Array value);
JsonValue(Object value);
[[nodiscard]] bool is_null() const;
[[nodiscard]] bool is_bool() const;
[[nodiscard]] bool is_number() const;
[[nodiscard]] bool is_string() const;
[[nodiscard]] bool is_array() const;
[[nodiscard]] bool is_object() const;
[[nodiscard]] bool as_bool() const;
[[nodiscard]] double as_number() const;
[[nodiscard]] const std::string& as_string() const;
[[nodiscard]] const Array& as_array() const;
[[nodiscard]] const Object& as_object() const;
[[nodiscard]] Array& as_array();
[[nodiscard]] Object& as_object();
[[nodiscard]] bool contains(const std::string& key) const;
[[nodiscard]] const JsonValue& at(const std::string& key) const;
private:
std::variant<std::nullptr_t, bool, double, std::string, Array, Object> data_{nullptr};
};
JsonValue parse_json(std::string_view text);
std::string to_json_string(const JsonValue& value, int indent = 2);
} // namespace mocca
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <memory>
#include "mocca/api.hpp"
namespace mocca {
/**
* Modern execution façade for the cascade physics kernel.
*
* The public surface stays intentionally small: callers provide a normalized
* `SimulationConfig`, and the implementation owns the setup, propagation, and
* line-collection workflow behind a value-oriented interface.
*/
class ModernCascadeKernel {
public:
explicit ModernCascadeKernel(const SimulationConfig& config);
~ModernCascadeKernel();
ModernCascadeKernel(ModernCascadeKernel&&) noexcept;
ModernCascadeKernel& operator=(ModernCascadeKernel&&) noexcept;
ModernCascadeKernel(const ModernCascadeKernel&) = delete;
ModernCascadeKernel& operator=(const ModernCascadeKernel&) = delete;
[[nodiscard]] SimulationResult run() const;
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
[[nodiscard]] SimulationResult run_modern_kernel(const SimulationConfig& config);
} // namespace mocca
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+586
View File
@@ -0,0 +1,586 @@
#include "mocca/api.hpp"
#include "mocca/kernel.hpp"
#include "embedded_tables.hpp"
#include <cmath>
#include <fstream>
#include <limits>
#include <sstream>
#include <stdexcept>
namespace mocca {
namespace {
std::string read_file(const std::filesystem::path& path) {
std::ifstream input(path);
if (!input) {
throw std::runtime_error("Unable to open file: " + path.string());
}
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
const JsonValue::Object& require_object(const JsonValue& value, std::string_view label) {
if (!value.is_object()) {
throw std::runtime_error(std::string(label) + " must be a JSON object");
}
return value.as_object();
}
const JsonValue::Array& require_array(const JsonValue& value, std::string_view label) {
if (!value.is_array()) {
throw std::runtime_error(std::string(label) + " must be a JSON array");
}
return value.as_array();
}
std::optional<const JsonValue*> find_value(const JsonValue::Object& object, const std::string& key) {
const auto it = object.find(key);
if (it == object.end()) {
return std::nullopt;
}
return &it->second;
}
double require_number(const JsonValue::Object& object, const std::string& key) {
const auto value = find_value(object, key);
if (!value.has_value() || !(*value)->is_number()) {
throw std::runtime_error("Missing numeric field: " + key);
}
return (*value)->as_number();
}
int checked_int(double value, std::string_view key) {
if (!std::isfinite(value) || std::trunc(value) != value) {
throw std::runtime_error(std::string(key) + " must be an integer");
}
if (value < static_cast<double>(std::numeric_limits<int>::min()) ||
value > static_cast<double>(std::numeric_limits<int>::max())) {
throw std::runtime_error(std::string(key) + " is outside the supported integer range");
}
return static_cast<int>(value);
}
int require_int(const JsonValue::Object& object, const std::string& key) {
return checked_int(require_number(object, key), key);
}
std::string require_string(const JsonValue::Object& object, const std::string& key) {
const auto value = find_value(object, key);
if (!value.has_value() || !(*value)->is_string()) {
throw std::runtime_error("Missing string field: " + key);
}
return (*value)->as_string();
}
std::optional<double> optional_number(const JsonValue::Object& object, const std::string& key) {
const auto value = find_value(object, key);
if (!value.has_value()) {
return std::nullopt;
}
if ((*value)->is_null()) {
return std::nullopt;
}
if (!(*value)->is_number()) {
throw std::runtime_error("Field must be numeric: " + key);
}
return (*value)->as_number();
}
std::optional<int> optional_int(const JsonValue::Object& object, const std::string& key) {
const auto value = optional_number(object, key);
if (!value.has_value()) {
return std::nullopt;
}
return checked_int(*value, key);
}
bool optional_bool(const JsonValue::Object& object, const std::string& key, bool default_value) {
const auto value = find_value(object, key);
if (!value.has_value()) {
return default_value;
}
if (!(*value)->is_bool()) {
throw std::runtime_error("Field must be bool: " + key);
}
return (*value)->as_bool();
}
std::vector<double> number_array(const JsonValue::Object& object, const std::string& key) {
const auto values = find_value(object, key);
if (!values.has_value()) {
return {};
}
std::vector<double> numbers;
for (const JsonValue& item : require_array(*(*values), key)) {
if (!item.is_number()) {
throw std::runtime_error("Array " + key + " must contain only numbers");
}
numbers.push_back(item.as_number());
}
return numbers;
}
std::optional<std::vector<double>> optional_number_array(
const JsonValue::Object& object,
const std::string& key) {
const auto value = find_value(object, key);
if (!value.has_value()) {
return std::nullopt;
}
std::vector<double> numbers;
for (const JsonValue& item : require_array(*(*value), key)) {
if (!item.is_number()) {
throw std::runtime_error("Array " + key + " must contain only numbers");
}
numbers.push_back(item.as_number());
}
return numbers;
}
std::optional<std::vector<int>> optional_int_array(
const JsonValue::Object& object,
const std::string& key) {
const auto values = optional_number_array(object, key);
if (!values.has_value()) {
return std::nullopt;
}
std::vector<int> ints;
ints.reserve(values->size());
for (double number : *values) {
ints.push_back(checked_int(number, key));
}
return ints;
}
JsonValue number_array_json(const std::vector<double>& values) {
JsonValue::Array array;
array.reserve(values.size());
for (double value : values) {
array.emplace_back(value);
}
return JsonValue(std::move(array));
}
JsonValue int_array_json(const std::vector<int>& values) {
JsonValue::Array array;
array.reserve(values.size());
for (int value : values) {
array.emplace_back(value);
}
return JsonValue(std::move(array));
}
CaptureMode parse_capture_mode(const std::string& raw_mode) {
if (raw_mode == "statistical_l") {
return CaptureMode::statistical_l;
}
if (raw_mode == "quadratic_l") {
return CaptureMode::quadratic_l;
}
if (raw_mode == "explicit_l") {
return CaptureMode::explicit_l;
}
if (raw_mode == "explicit_nl") {
return CaptureMode::explicit_nl;
}
if (raw_mode == "legacy_empty") {
return CaptureMode::legacy_empty;
}
throw std::runtime_error("Unsupported capture mode: " + raw_mode);
}
std::string capture_mode_name(CaptureMode mode) {
switch (mode) {
case CaptureMode::statistical_l:
return "statistical_l";
case CaptureMode::quadratic_l:
return "quadratic_l";
case CaptureMode::explicit_l:
return "explicit_l";
case CaptureMode::explicit_nl:
return "explicit_nl";
case CaptureMode::legacy_empty:
return "legacy_empty";
}
throw std::runtime_error("Unhandled capture mode");
}
JsonValue dirac_energy_json(const DiracEnergy& entry) {
return JsonValue::Object{
{"binding_kev", entry.binding_kev},
{"kappa", entry.kappa},
{"n", entry.n},
{"vacuum_polarization_kev", entry.vacuum_polarization_kev},
};
}
JsonValue nl_weight_json(const NlWeight& entry) {
return JsonValue::Object{
{"l", entry.l},
{"n", entry.n},
{"weight", entry.weight},
};
}
JsonValue optional_number_json(const std::optional<double>& value) {
if (!value.has_value()) {
return JsonValue(nullptr);
}
return JsonValue(*value);
}
} // namespace
SimulationConfig parse_config_text(std::string_view text) {
const JsonValue root = parse_json(text);
const JsonValue::Object& object = require_object(root, "root");
SimulationConfig config;
if (const auto schema = optional_int(object, "schema_version"); schema.has_value()) {
config.schema_version = *schema;
}
if (config.schema_version != 1) {
throw std::runtime_error("Unsupported schema_version: " + std::to_string(config.schema_version));
}
if (const auto metadata_value = find_value(object, "metadata"); metadata_value.has_value()) {
const JsonValue::Object& metadata = require_object(*(*metadata_value), "metadata");
if (const auto case_name = find_value(metadata, "case_name"); case_name.has_value()) {
if (!(*case_name)->is_string()) {
throw std::runtime_error("metadata.case_name must be a string");
}
config.metadata.case_name = (*case_name)->as_string();
}
}
const JsonValue::Object& atom = require_object(object.at("atom"), "atom");
config.atom.atomic_number = require_number(atom, "atomic_number");
config.atom.effective_shell_charges = number_array(atom, "effective_shell_charges");
config.atom.binding_energies_ev = number_array(atom, "binding_energies_ev");
config.atom.atomic_mass = require_number(atom, "atomic_mass");
config.atom.exact_mass_number = optional_number(atom, "exact_mass_number");
if (const auto masses_value = find_value(object, "masses"); masses_value.has_value()) {
const JsonValue::Object& masses = require_object(*(*masses_value), "masses");
if (const auto value = optional_number(masses, "muon_electron_masses"); value.has_value()) {
config.masses.muon_electron_masses = *value;
}
if (const auto value = optional_number(masses, "electron_mass_ev"); value.has_value()) {
config.masses.electron_mass_ev = *value;
}
if (const auto value = optional_number(masses, "nucleon_mass_mev"); value.has_value()) {
config.masses.nucleon_mass_mev = *value;
}
}
if (const auto transitions_value = find_value(object, "transitions"); transitions_value.has_value()) {
const JsonValue::Object& transitions = require_object(*(*transitions_value), "transitions");
config.transitions.two_p_to_one_s_energy_ev = optional_number(transitions, "two_p_to_one_s_energy_ev");
config.transitions.two_s_to_two_p_split_ev = optional_number(transitions, "two_s_to_two_p_split_ev");
if (const auto entries_value = find_value(transitions, "dirac_energies"); entries_value.has_value()) {
for (const JsonValue& entry_value : require_array(*(*entries_value), "transitions.dirac_energies")) {
const JsonValue::Object& entry = require_object(entry_value, "dirac energy entry");
config.transitions.dirac_energies.push_back(DiracEnergy{
require_int(entry, "n"),
require_int(entry, "kappa"),
optional_number(entry, "vacuum_polarization_kev").value_or(0.0),
require_number(entry, "binding_kev"),
});
}
}
}
const JsonValue::Object& capture = require_object(object.at("capture"), "capture");
config.capture.mode = parse_capture_mode(require_string(capture, "mode"));
config.capture.n_max = require_int(capture, "n_max");
if (const auto value = optional_number(capture, "alpha"); value.has_value()) {
config.capture.alpha = *value;
}
config.capture.quadratic_coefficients = number_array(capture, "quadratic_coefficients");
config.capture.l_weights = number_array(capture, "l_weights");
if (const auto nl_weights_value = find_value(capture, "nl_weights"); nl_weights_value.has_value()) {
for (const JsonValue& entry_value : require_array(*(*nl_weights_value), "capture.nl_weights")) {
const JsonValue::Object& entry = require_object(entry_value, "capture.nl_weights entry");
config.capture.nl_weights.push_back(NlWeight{
require_int(entry, "n"),
require_int(entry, "l"),
require_number(entry, "weight"),
});
}
}
if (const auto channels_value = find_value(object, "channels"); channels_value.has_value()) {
const JsonValue::Object& channels = require_object(*(*channels_value), "channels");
if (const auto values = optional_int_array(channels, "case_counts"); values.has_value()) {
config.channels.case_counts = *values;
}
if (const auto values = optional_int_array(channels, "monopole_shells"); values.has_value()) {
config.channels.monopole_shells = *values;
}
if (const auto values = optional_int_array(channels, "dipole_shells"); values.has_value()) {
config.channels.dipole_shells = *values;
}
if (const auto values = optional_int_array(channels, "quadrupole_shells"); values.has_value()) {
config.channels.quadrupole_shells = *values;
}
if (const auto values = optional_int_array(channels, "octupole_shells"); values.has_value()) {
config.channels.octupole_shells = *values;
}
if (const auto values = optional_int_array(channels, "dipole_subshell_channels"); values.has_value()) {
config.channels.dipole_subshell_channels = *values;
}
if (const auto values = optional_int_array(channels, "quadrupole_subshell_channels");
values.has_value()) {
config.channels.quadrupole_subshell_channels = *values;
}
if (const auto values = optional_int_array(channels, "octupole_subshell_channels");
values.has_value()) {
config.channels.octupole_subshell_channels = *values;
}
if (const auto values = optional_int_array(channels, "dipole_penetration_codes");
values.has_value()) {
config.channels.dipole_penetration_codes = *values;
}
if (const auto values = optional_int_array(channels, "quadrupole_penetration_codes");
values.has_value()) {
config.channels.quadrupole_penetration_codes = *values;
}
if (const auto values = optional_int_array(channels, "octupole_penetration_codes");
values.has_value()) {
config.channels.octupole_penetration_codes = *values;
}
if (const auto values = optional_int_array(channels, "dipole_penetration_avg_n_cutoffs");
values.has_value()) {
config.channels.dipole_penetration_avg_n_cutoffs = *values;
}
if (const auto values =
optional_int_array(channels, "quadrupole_penetration_avg_n_cutoffs");
values.has_value()) {
config.channels.quadrupole_penetration_avg_n_cutoffs = *values;
}
if (const auto values = optional_int_array(channels, "octupole_penetration_avg_n_cutoffs");
values.has_value()) {
config.channels.octupole_penetration_avg_n_cutoffs = *values;
}
}
if (const auto shell_value = find_value(object, "shell_model"); shell_value.has_value()) {
const JsonValue::Object& shell_model = require_object(*(*shell_value), "shell_model");
if (const auto values = optional_number_array(shell_model, "subshell_populations");
values.has_value()) {
config.shell_model.subshell_populations = *values;
}
if (const auto values = optional_int_array(shell_model, "refill_codes"); values.has_value()) {
config.shell_model.refill_codes = *values;
}
if (const auto values = optional_number_array(shell_model, "penetration_cutoffs");
values.has_value()) {
config.shell_model.penetration_cutoffs = *values;
}
if (const auto value = optional_number(shell_model, "width_k_ev"); value.has_value()) {
config.shell_model.width_k_ev = *value;
}
config.shell_model.track_polarization = optional_bool(shell_model, "track_polarization", true);
}
if (const auto reporting_value = find_value(object, "reporting"); reporting_value.has_value()) {
const JsonValue::Object& reporting = require_object(*(*reporting_value), "reporting");
if (const auto value = optional_number(reporting, "line_energy_min_mev"); value.has_value()) {
config.reporting.line_energy_min_mev = *value;
}
if (const auto value = optional_number(reporting, "line_energy_max_mev"); value.has_value()) {
config.reporting.line_energy_max_mev = *value;
}
if (const auto value = optional_number(reporting, "line_intensity_threshold"); value.has_value()) {
config.reporting.line_intensity_threshold = *value;
}
if (const auto value = optional_number(reporting, "energy_resolution_mev"); value.has_value()) {
config.reporting.energy_resolution_mev = *value;
}
}
if (const auto model_value = find_value(object, "model"); model_value.has_value()) {
const JsonValue::Object& model = require_object(*(*model_value), "model");
if (const auto value = optional_number(model, "factorial_divider"); value.has_value()) {
config.model.factorial_divider = *value;
}
}
if (const auto numerics_value = find_value(object, "numerics"); numerics_value.has_value()) {
const JsonValue::Object& numerics = require_object(*(*numerics_value), "numerics");
if (const auto value = optional_int(numerics, "matrix_element_precision_digits");
value.has_value()) {
config.numerics.matrix_element_precision_digits = *value;
} else if (const auto legacy_value = optional_int(numerics, "stable_precision_digits");
legacy_value.has_value()) {
config.numerics.matrix_element_precision_digits = *legacy_value;
}
}
if (config.numerics.matrix_element_precision_digits <= 0) {
throw std::runtime_error(
"numerics.matrix_element_precision_digits must be a positive integer");
}
return config;
}
SimulationConfig load_config(const std::filesystem::path& path) {
return parse_config_text(read_file(path));
}
JsonValue to_json(const SimulationConfig& config) {
JsonValue::Array dirac_energies;
dirac_energies.reserve(config.transitions.dirac_energies.size());
for (const DiracEnergy& entry : config.transitions.dirac_energies) {
dirac_energies.push_back(dirac_energy_json(entry));
}
JsonValue::Array nl_weights;
nl_weights.reserve(config.capture.nl_weights.size());
for (const NlWeight& entry : config.capture.nl_weights) {
nl_weights.push_back(nl_weight_json(entry));
}
return JsonValue::Object{
{"schema_version", config.schema_version},
{"metadata", JsonValue::Object{
{"case_name", config.metadata.case_name},
}},
{"atom", JsonValue::Object{
{"atomic_number", config.atom.atomic_number},
{"atomic_mass", config.atom.atomic_mass},
{"binding_energies_ev", number_array_json(config.atom.binding_energies_ev)},
{"effective_shell_charges", number_array_json(config.atom.effective_shell_charges)},
{"exact_mass_number", optional_number_json(config.atom.exact_mass_number)},
}},
{"masses", JsonValue::Object{
{"electron_mass_ev", config.masses.electron_mass_ev},
{"muon_electron_masses", config.masses.muon_electron_masses},
{"nucleon_mass_mev", config.masses.nucleon_mass_mev},
}},
{"transitions", JsonValue::Object{
{"dirac_energies", JsonValue(std::move(dirac_energies))},
{"two_p_to_one_s_energy_ev", optional_number_json(config.transitions.two_p_to_one_s_energy_ev)},
{"two_s_to_two_p_split_ev", optional_number_json(config.transitions.two_s_to_two_p_split_ev)},
}},
{"capture", JsonValue::Object{
{"alpha", config.capture.alpha},
{"l_weights", number_array_json(config.capture.l_weights)},
{"mode", capture_mode_name(config.capture.mode)},
{"n_max", config.capture.n_max},
{"nl_weights", JsonValue(std::move(nl_weights))},
{"quadratic_coefficients", number_array_json(config.capture.quadratic_coefficients)},
}},
{"channels", JsonValue::Object{
{"case_counts", int_array_json(config.channels.case_counts)},
{"dipole_penetration_avg_n_cutoffs", int_array_json(config.channels.dipole_penetration_avg_n_cutoffs)},
{"dipole_penetration_codes", int_array_json(config.channels.dipole_penetration_codes)},
{"dipole_shells", int_array_json(config.channels.dipole_shells)},
{"dipole_subshell_channels", int_array_json(config.channels.dipole_subshell_channels)},
{"monopole_shells", int_array_json(config.channels.monopole_shells)},
{"octupole_penetration_avg_n_cutoffs", int_array_json(config.channels.octupole_penetration_avg_n_cutoffs)},
{"octupole_penetration_codes", int_array_json(config.channels.octupole_penetration_codes)},
{"octupole_shells", int_array_json(config.channels.octupole_shells)},
{"octupole_subshell_channels", int_array_json(config.channels.octupole_subshell_channels)},
{"quadrupole_penetration_avg_n_cutoffs", int_array_json(config.channels.quadrupole_penetration_avg_n_cutoffs)},
{"quadrupole_penetration_codes", int_array_json(config.channels.quadrupole_penetration_codes)},
{"quadrupole_shells", int_array_json(config.channels.quadrupole_shells)},
{"quadrupole_subshell_channels", int_array_json(config.channels.quadrupole_subshell_channels)},
}},
{"shell_model", JsonValue::Object{
{"penetration_cutoffs", number_array_json(config.shell_model.penetration_cutoffs)},
{"refill_codes", int_array_json(config.shell_model.refill_codes)},
{"subshell_populations", number_array_json(config.shell_model.subshell_populations)},
{"track_polarization", config.shell_model.track_polarization},
{"width_k_ev", config.shell_model.width_k_ev},
}},
{"reporting", JsonValue::Object{
{"energy_resolution_mev", config.reporting.energy_resolution_mev},
{"line_energy_max_mev", config.reporting.line_energy_max_mev},
{"line_energy_min_mev", config.reporting.line_energy_min_mev},
{"line_intensity_threshold", config.reporting.line_intensity_threshold},
}},
{"model", JsonValue::Object{
{"factorial_divider", config.model.factorial_divider},
}},
{"numerics", JsonValue::Object{
{"matrix_element_precision_digits", config.numerics.matrix_element_precision_digits},
}},
};
}
JsonValue to_json(const SimulationResult& result) {
JsonValue::Array lines;
lines.reserve(result.lines.size());
for (const TransitionLine& line : result.lines) {
lines.push_back(JsonValue::Object{
{"energy_kev", line.energy_kev},
{"intensity", line.intensity},
{"j1_twice", line.j1_twice},
{"j2_twice", line.j2_twice},
{"l1", line.l1},
{"l2", line.l2},
{"multipole", line.multipole},
{"n1", line.n1},
{"n2", line.n2},
});
}
JsonValue::Array states;
states.reserve(result.states.size());
for (const StateSummary& state : result.states) {
states.push_back(JsonValue::Object{
{"k_electrons", state.k_electrons},
{"l", state.l},
{"l_electrons", state.l_electrons},
{"m_electrons", state.m_electrons},
{"n", state.n},
{"polar_down", optional_number_json(state.polar_down)},
{"polar_up", optional_number_json(state.polar_up)},
{"population", state.population},
{"rad_to_auger", optional_number_json(state.rad_to_auger)},
{"spin_orbit_ev", optional_number_json(state.spin_orbit_ev)},
{"width_ev", optional_number_json(state.width_ev)},
});
}
JsonValue::Array warnings;
warnings.reserve(result.warnings.size());
for (const std::string& warning : result.warnings) {
warnings.emplace_back(warning);
}
return JsonValue::Object{
{"lines", JsonValue(std::move(lines))},
{"lyman_sum", result.lyman_sum},
{"num_lines", result.num_lines},
{"states", JsonValue(std::move(states))},
{"warnings", JsonValue(std::move(warnings))},
};
}
JsonValue to_json(const SimulationArtifact& artifact) {
return JsonValue::Object{
{"coefficient_table_id", artifact.coefficient_table_id},
{"implementation_name", artifact.implementation_name},
{"input", to_json(artifact.input)},
{"numerical_backend", artifact.numerical_backend},
{"result", to_json(artifact.result)},
{"schema_version", artifact.schema_version},
{"matrix_element_precision_digits", artifact.matrix_element_precision_digits},
};
}
SimulationArtifact run_simulation(const SimulationConfig& config) {
SimulationArtifact artifact;
artifact.coefficient_table_id = std::string(detail::kBundledCoefficientTableId);
artifact.matrix_element_precision_digits = config.numerics.matrix_element_precision_digits;
artifact.input = config;
artifact.result = run_modern_kernel(config);
return artifact;
}
} // namespace mocca
+59
View File
@@ -0,0 +1,59 @@
#include "mocca/api.hpp"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
namespace {
void print_usage() {
std::cerr << "Usage: mocca <config.json> [--output result.json]\n";
}
void write_text(const std::filesystem::path& path, const std::string& text) {
std::ofstream output(path);
if (!output) {
throw std::runtime_error("Unable to open output file: " + path.string());
}
output << text;
}
} // namespace
int main(int argc, char** argv) {
try {
if (argc < 2) {
print_usage();
return 2;
}
std::filesystem::path config_path = argv[1];
std::filesystem::path output_path;
for (int i = 2; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--output" && i + 1 < argc) {
output_path = argv[++i];
} else {
throw std::runtime_error("Unknown or incomplete argument: " + arg);
}
}
const mocca::SimulationConfig config = mocca::load_config(config_path);
const mocca::SimulationArtifact artifact = mocca::run_simulation(config);
const std::string payload =
mocca::to_json_string(mocca::to_json(artifact), 2);
if (!output_path.empty()) {
write_text(output_path, payload);
} else {
std::cout << payload;
}
return 0;
} catch (const std::exception& exc) {
std::cerr << exc.what() << '\n';
return 1;
}
}
+299
View File
@@ -0,0 +1,299 @@
#include "config_bridge.hpp"
#include <algorithm>
#include <cmath>
#include <sstream>
#include <stdexcept>
namespace mocca::detail {
namespace {
template <typename T>
void set_one_based(std::vector<T>& destination, const std::vector<T>& source) {
const std::size_t capacity = destination.empty() ? 0 : destination.size() - 1;
if (source.size() > capacity) {
throw std::runtime_error("Input vector exceeds the supported capacity");
}
for (std::size_t i = 0; i < source.size(); ++i) {
destination[i + 1] = source[i];
}
}
template <typename T>
void require_exact_size(
const std::vector<T>& values,
std::size_t expected_size,
std::string_view field_name) {
if (values.size() != expected_size) {
std::ostringstream out;
out << field_name << " must contain exactly " << expected_size
<< " value(s); received " << values.size();
throw std::runtime_error(out.str());
}
}
void require_int_range(
int value,
int min_value,
int max_value,
std::string_view field_name) {
if (value < min_value || value > max_value) {
std::ostringstream out;
out << field_name << " must be between " << min_value << " and " << max_value
<< "; received " << value;
throw std::runtime_error(out.str());
}
}
void require_values_in_range(
const std::vector<int>& values,
int min_value,
int max_value,
std::string_view field_name) {
for (std::size_t index = 0; index < values.size(); ++index) {
if (values[index] < min_value || values[index] > max_value) {
std::ostringstream out;
out << field_name << "[" << index << "] must be between " << min_value << " and "
<< max_value << "; received " << values[index];
throw std::runtime_error(out.str());
}
}
}
void apply_dirac_energies(
KernelState& cfg,
const std::vector<DiracEnergy>& dirac_energies) {
if (dirac_energies.empty()) {
return;
}
cfg.icc = 1;
for (const DiracEnergy& entry : dirac_energies) {
if (entry.n < 0 || entry.n >= static_cast<int>(cfg.energy.size())) {
throw std::runtime_error("Dirac energy n is out of supported range");
}
if (entry.kappa < 0 || entry.kappa >= static_cast<int>(cfg.energy[entry.n].size())) {
throw std::runtime_error("Dirac energy kappa is out of supported range");
}
double value = entry.binding_kev + entry.vacuum_polarization_kev;
if (entry.vacuum_polarization_kev < 0.0) {
cfg.idr += 1;
}
cfg.energy[entry.n][entry.kappa] = value;
}
}
void apply_capture(KernelState& cfg, const CaptureConfig& capture) {
cfg.nmax = capture.n_max;
cfg.ip8 = 0;
cfg.nopt = 1;
std::fill(cfg.pl.begin(), cfg.pl.end(), 0.0);
std::fill(cfg.pln.begin(), cfg.pln.end(), 0.0);
switch (capture.mode) {
case CaptureMode::statistical_l:
cfg.nopt = 0;
cfg.alexp = capture.alpha;
break;
case CaptureMode::quadratic_l:
cfg.nopt = 2;
require_exact_size(
capture.quadratic_coefficients,
2,
"capture.quadratic_coefficients");
cfg.cl1 = capture.quadratic_coefficients[0];
cfg.cl2 = capture.quadratic_coefficients[1];
break;
case CaptureMode::explicit_l:
require_exact_size(
capture.l_weights,
static_cast<std::size_t>(capture.n_max),
"capture.l_weights");
for (std::size_t index = 0; index < capture.l_weights.size(); ++index) {
if (index + 1 >= cfg.pl.size()) {
break;
}
cfg.pl[index + 1] = capture.l_weights[index];
}
break;
case CaptureMode::explicit_nl:
cfg.ip8 = 1;
if (capture.nl_weights.empty()) {
throw std::runtime_error("explicit_nl mode requires nl_weights");
}
for (const NlWeight& weight : capture.nl_weights) {
if (weight.n < 1 || weight.n > cfg.nmax || weight.l < 0 || weight.l >= weight.n) {
throw std::runtime_error("explicit_nl weights contain an invalid (n,l) state");
}
const int index = state_index(weight.n, weight.l);
if (index >= static_cast<int>(cfg.pln.size())) {
throw std::runtime_error("explicit_nl state exceeds the supported n range");
}
cfg.pln[index] = weight.weight;
}
break;
case CaptureMode::legacy_empty:
cfg.nopt = 4;
break;
}
}
} // namespace
KernelState build_kernel_config(
const NumericTables& numeric_tables,
const SimulationConfig& config) {
KernelState cfg(numeric_tables);
require_exact_size(config.atom.effective_shell_charges, 3, "atom.effective_shell_charges");
require_exact_size(config.atom.binding_energies_ev, 3, "atom.binding_energies_ev");
if (config.capture.n_max < 1 || config.capture.n_max > 20) {
throw std::runtime_error("capture.n_max must be between 1 and 20");
}
cfg.z = config.atom.atomic_number;
cfg.zsk = config.atom.effective_shell_charges[0];
cfg.zsl = config.atom.effective_shell_charges[1];
cfg.zsm = config.atom.effective_shell_charges[2];
cfg.be[1] = config.atom.binding_energies_ev[0];
cfg.be[2] = config.atom.binding_energies_ev[1];
cfg.be[3] = config.atom.binding_energies_ev[2];
cfg.a = config.atom.atomic_mass;
if (config.atom.exact_mass_number.has_value()) {
cfg.amassa = *config.atom.exact_mass_number;
}
cfg.amassm = config.masses.muon_electron_masses;
cfg.amasse = config.masses.electron_mass_ev;
cfg.amassn = config.masses.nucleon_mass_mev;
if (!std::isfinite(config.model.factorial_divider) || config.model.factorial_divider <= 0.0) {
throw std::runtime_error("model.factorial_divider must be finite and strictly positive");
}
cfg.fd = config.model.factorial_divider;
if (config.transitions.two_p_to_one_s_energy_ev.has_value()) {
cfg.d2p1s = *config.transitions.two_p_to_one_s_energy_ev;
}
if (config.transitions.two_s_to_two_p_split_ev.has_value()) {
cfg.esp = *config.transitions.two_s_to_two_p_split_ev;
}
apply_dirac_energies(cfg, config.transitions.dirac_energies);
require_exact_size(config.channels.case_counts, 4, "channels.case_counts");
require_int_range(
config.channels.case_counts[0],
0,
static_cast<int>(cfg.nn0.size()) - 1,
"channels.case_counts[0]");
require_int_range(
config.channels.case_counts[1],
0,
static_cast<int>(cfg.nn1.size()) - 1,
"channels.case_counts[1]");
require_int_range(
config.channels.case_counts[2],
0,
static_cast<int>(cfg.nn2.size()) - 1,
"channels.case_counts[2]");
require_int_range(
config.channels.case_counts[3],
0,
static_cast<int>(cfg.nn3.size()) - 1,
"channels.case_counts[3]");
cfg.k0 = config.channels.case_counts[0];
cfg.k1 = config.channels.case_counts[1];
cfg.k2 = config.channels.case_counts[2];
cfg.k3 = config.channels.case_counts[3];
require_exact_size(
config.channels.monopole_shells,
static_cast<std::size_t>(cfg.k0),
"channels.monopole_shells");
require_exact_size(
config.channels.dipole_shells,
static_cast<std::size_t>(cfg.k1),
"channels.dipole_shells");
require_exact_size(
config.channels.quadrupole_shells,
static_cast<std::size_t>(cfg.k2),
"channels.quadrupole_shells");
require_exact_size(
config.channels.octupole_shells,
static_cast<std::size_t>(cfg.k3),
"channels.octupole_shells");
require_exact_size(
config.channels.dipole_subshell_channels,
static_cast<std::size_t>(cfg.k1),
"channels.dipole_subshell_channels");
require_exact_size(
config.channels.quadrupole_subshell_channels,
static_cast<std::size_t>(cfg.k2),
"channels.quadrupole_subshell_channels");
require_exact_size(
config.channels.octupole_subshell_channels,
static_cast<std::size_t>(cfg.k3),
"channels.octupole_subshell_channels");
require_exact_size(
config.channels.dipole_penetration_codes,
static_cast<std::size_t>(cfg.k1),
"channels.dipole_penetration_codes");
require_exact_size(
config.channels.quadrupole_penetration_codes,
static_cast<std::size_t>(cfg.k2),
"channels.quadrupole_penetration_codes");
require_exact_size(
config.channels.octupole_penetration_codes,
static_cast<std::size_t>(cfg.k3),
"channels.octupole_penetration_codes");
require_exact_size(
config.channels.dipole_penetration_avg_n_cutoffs,
static_cast<std::size_t>(cfg.k1),
"channels.dipole_penetration_avg_n_cutoffs");
require_exact_size(
config.channels.quadrupole_penetration_avg_n_cutoffs,
static_cast<std::size_t>(cfg.k2),
"channels.quadrupole_penetration_avg_n_cutoffs");
require_exact_size(
config.channels.octupole_penetration_avg_n_cutoffs,
static_cast<std::size_t>(cfg.k3),
"channels.octupole_penetration_avg_n_cutoffs");
require_values_in_range(config.channels.monopole_shells, 1, 3, "channels.monopole_shells");
require_values_in_range(config.channels.dipole_shells, 0, 3, "channels.dipole_shells");
require_values_in_range(
config.channels.quadrupole_shells,
0,
3,
"channels.quadrupole_shells");
require_values_in_range(config.channels.octupole_shells, 0, 3, "channels.octupole_shells");
set_one_based(cfg.nn0, config.channels.monopole_shells);
set_one_based(cfg.nn1, config.channels.dipole_shells);
set_one_based(cfg.nn2, config.channels.quadrupole_shells);
set_one_based(cfg.nn3, config.channels.octupole_shells);
set_one_based(cfg.m1, config.channels.dipole_subshell_channels);
set_one_based(cfg.m2, config.channels.quadrupole_subshell_channels);
set_one_based(cfg.m3, config.channels.octupole_subshell_channels);
set_one_based(cfg.ip1, config.channels.dipole_penetration_codes);
set_one_based(cfg.ip2, config.channels.quadrupole_penetration_codes);
set_one_based(cfg.ip3, config.channels.octupole_penetration_codes);
set_one_based(cfg.iq1, config.channels.dipole_penetration_avg_n_cutoffs);
set_one_based(cfg.iq2, config.channels.quadrupole_penetration_avg_n_cutoffs);
set_one_based(cfg.iq3, config.channels.octupole_penetration_avg_n_cutoffs);
require_exact_size(config.shell_model.subshell_populations, 6, "shell_model.subshell_populations");
require_exact_size(config.shell_model.refill_codes, 3, "shell_model.refill_codes");
require_exact_size(config.shell_model.penetration_cutoffs, 4, "shell_model.penetration_cutoffs");
set_one_based(cfg.pop, config.shell_model.subshell_populations);
set_one_based(cfg.ipc, config.shell_model.refill_codes);
set_one_based(cfg.yc, config.shell_model.penetration_cutoffs);
cfg.widthk = config.shell_model.width_k_ev;
cfg.ipol = config.shell_model.track_polarization ? 0 : 1;
cfg.elow = config.reporting.line_energy_min_mev;
cfg.ehigh = config.reporting.line_energy_max_mev;
cfg.climit = config.reporting.line_intensity_threshold;
cfg.eres = config.reporting.energy_resolution_mev;
apply_capture(cfg, config.capture);
return cfg;
}
} // namespace mocca::detail
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "mocca/api.hpp"
#include "physics_engine.hpp"
namespace mocca::detail {
[[nodiscard]] KernelState build_kernel_config(
const NumericTables& numeric_tables,
const SimulationConfig& config);
} // namespace mocca::detail
+395
View File
@@ -0,0 +1,395 @@
// Generated by tools/export_embedded_tables.py from muon00.f block-data tables.
// Do not edit this file by hand; regenerate it when the bundled coefficient source changes.
#include "embedded_tables.hpp"
namespace mocca::detail {
NumericTables bundled_numeric_tables() {
return NumericTables{
{"COEDP", std::vector<double>{
21.33333,
42.66667,
3.555556,
113.7778,
7.111111,
56.88889,
1024,
22.75556,
1228.8
}},
{"COEQ", std::vector<double>{
0.06666667,
0.08888889,
0.0006944444,
0.01,
0.0009375,
4.064421e-05,
0.00351166,
6.503074e-05,
0.02107,
9.290105e-05,
2.064468e-06
}},
{"COEO", std::vector<double>{
0.001693122,
0.01015873,
1.984127e-05,
0.000212585,
3.985969e-07,
5.734633e-08,
1.47462e-05,
5.461556e-09,
2.654316e-05,
7.646177e-07,
1.365389e-06
}},
{"COED", std::vector<double>{
1.333333,
21.33333,
10.66667,
7.111111
}},
{"EXPMON", std::vector<double>{
4.531799,
4.706453,
4.736786,
3.471234,
3.150354,
3.052991,
4.703517,
3.45911,
3.249048,
3.150354,
2.977822,
2.917974,
3.312419,
2.695684,
2.533755,
4.914818,
2.970119,
2.700216,
6.548446,
3.199174,
2.830713,
2.695684,
2.41503,
2.323143,
2.974074,
2.526711,
2.40295,
2.41503,
2.249824,
2.188693
}},
{"EXPDIP", std::vector<double>{
3.766185,
4.397381,
4.511374,
2.429543,
2.72324,
2.747787,
2.976262,
2.927947,
2.886848,
4.07693,
3.354237,
3.192531,
2.72324,
2.747093,
2.736171,
2.064114,
2.209579,
2.187568,
2.546045,
2.374398,
2.298389,
2.876576,
2.498902,
2.38535,
3.993301,
2.876576,
2.652421,
5.778643,
3.134388,
2.797892,
2.209579,
2.154546,
2.118914,
2.374398,
2.234588,
2.178575,
2.876576,
2.498902,
2.38535,
2.154546,
2.084353,
2.051376
}},
{"EXPQUA", std::vector<double>{
3.531162,
4.240205,
4.388131,
2.543817,
2.683365,
2.694792,
2.131239,
2.520683,
2.5795,
2.566813,
2.816281,
2.81955,
2.520683,
2.607425,
2.618563,
1.726067,
1.977932,
1.998559,
2.061514,
2.104925,
2.087356,
2.283507,
2.199915,
2.155173,
2.183688,
2.283507,
2.24505,
2.663082,
2.438142,
2.347529,
1.977932,
1.998217,
1.987446,
2.104925,
2.062287,
2.036313,
4.34099,
2.986053,
2.726274,
2.283507,
2.199915,
2.155173,
1.998217,
1.972396,
1.956081
}},
{"EXPOCT", std::vector<double>{
3.429305,
4.153371,
4.322206,
1.982441,
2.395708,
2.470619,
2.341001,
2.538774,
2.572215,
2.195239,
2.571516,
2.623072,
2.395708,
2.51249,
2.537093,
1.558161,
1.837921,
1.876801,
1.841217,
1.946305,
1.953014,
2.017891,
2.0263,
2.011369,
1.778407,
2.017891,
2.032372,
2.117493,
2.140989,
2.117166,
1.837921,
1.89234,
1.895838,
1.946305,
1.948185,
1.938261,
2.252893,
2.331797,
2.283271,
2.017891,
2.0263,
2.011369,
1.89234,
1.921936,
1.898511
}},
{"COEMON", std::vector<double>{
0.9278462,
-0.04686876,
0.00260669,
0.3258145,
-0.01650726,
0.0009192955,
-0.08728775,
0.005557533,
-0.0003299306,
0.01650726,
-0.000788986,
1.644805e-05,
0.1837462,
-0.009081039000000001,
0.0005039261,
-0.07674657,
0.004090333,
-0.000241485,
0.006976183,
-0.0003308179,
2.022719e-05,
0.009886183,
-0.0004700158,
9.786634e-06,
-0.001115213,
5.895603e-05,
-1.271168e-06,
4.52273e-05,
-1.882362e-06,
2.089122e-08
}},
{"COEDIP", std::vector<double>{
0.09864278,
-0.003552196,
6.908681e-05,
0.02415974,
-0.0008812568999999999,
1.721286e-05,
-0.006819236,
0.0003101739,
-6.390954e-06,
0.07532858000000001,
-0.004445121,
0.0002592835,
0.0002203142,
-8.210685e-06,
8.801341e-08,
0.0323546,
-0.001181545,
2.3038e-05,
-0.01225112,
0.0005542601,
-1.140757e-05,
0.0008892663,
-4.580119e-05,
9.800079e-07,
0.01211143,
-0.0006669497,
3.872454e-05,
-0.001512719,
7.933719e-05,
-4.808937e-06,
4.376093e-05,
-1.627056e-06,
1.743224e-08,
-5.132038e-06,
2.09574e-07,
-2.313305e-09,
0.0002223166,
-1.14503e-05,
2.45002e-07,
9.039198e-08,
-3.079553e-09,
2.088144e-11
}},
{"COEQUA", std::vector<double>{
0.1411675,
-0.003944496,
3.94504e-05,
0.3270497,
-0.01133197,
0.0001194093,
-0.2776085,
0.007846003000000001,
-7.866167e-05,
0.4601989,
-0.01831626,
0.000367533,
0.003487113,
-0.0001060614,
6.944398e-07,
1.25312,
-0.03541941,
0.0003549579,
-0.4926037,
0.01705249,
-0.0001796112,
0.03681114,
-0.001438755,
1.569441e-05,
0.462617,
-0.01840557,
0.000369003,
-0.04608811,
0.00222026,
-4.671028e-05,
0.00787098,
-0.0002392062,
1.565542e-06,
-0.0009473607,
3.134314e-05,
-2.107953e-07,
0.04492433,
-0.002608238,
0.0001552426,
0.009202785999999999,
-0.0003596887,
3.923602e-06,
5.980154e-05,
-1.722622e-06,
7.889632e-09
}},
{"COEOCT", std::vector<double>{
0.01835095,
-0.0004182848,
2.558331e-06,
0.1444508,
-0.003328672,
2.037957e-05,
-0.04367688,
0.001223204,
-7.847840000000001e-06,
0.3021241,
-0.008987655000000001,
9.179068999999999e-05,
0.0044383,
-0.000114052,
5.042711e-07,
1.467168,
-0.03379181,
0.0002068045,
-0.592403,
0.01655453,
-0.0001061699,
0.04508567,
-0.001416945,
9.394653e-06,
0.6820167,
-0.02028855,
0.000207118,
-0.0691095,
0.002487891,
-2.660315e-05,
0.0225287,
-0.000578585,
2.556806e-06,
-0.002759089,
7.677244000000001e-05,
-3.479514e-07,
0.3778402,
-0.01582449,
0.0003229544,
0.004508567,
-0.0001416945,
9.394653e-07,
1.285744e-06,
-3.25799e-08,
1.067759e-10
}},
};
}
} // namespace mocca::detail
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <string_view>
#include "physics_engine.hpp"
namespace mocca::detail {
// Bump this identifier whenever the embedded coefficient tables are regenerated
// from a materially different published source.
inline constexpr std::string_view kBundledCoefficientTableId = "aama_v1_0_block_data_v1";
[[nodiscard]] NumericTables bundled_numeric_tables();
} // namespace mocca::detail
+515
View File
@@ -0,0 +1,515 @@
#include "mocca/json.hpp"
#include <cctype>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <limits>
#include <sstream>
namespace mocca {
JsonValue::JsonValue(std::nullptr_t) : data_(nullptr) {}
JsonValue::JsonValue(bool value) : data_(value) {}
JsonValue::JsonValue(double value) : data_(value) {}
JsonValue::JsonValue(int value) : data_(static_cast<double>(value)) {}
JsonValue::JsonValue(std::string value) : data_(std::move(value)) {}
JsonValue::JsonValue(const char* value) {
if (value == nullptr) {
throw std::runtime_error("JSON string pointer is null");
}
data_ = std::string(value);
}
JsonValue::JsonValue(Array value) : data_(std::move(value)) {}
JsonValue::JsonValue(Object value) : data_(std::move(value)) {}
bool JsonValue::is_null() const {
return std::holds_alternative<std::nullptr_t>(data_);
}
bool JsonValue::is_bool() const {
return std::holds_alternative<bool>(data_);
}
bool JsonValue::is_number() const {
return std::holds_alternative<double>(data_);
}
bool JsonValue::is_string() const {
return std::holds_alternative<std::string>(data_);
}
bool JsonValue::is_array() const {
return std::holds_alternative<Array>(data_);
}
bool JsonValue::is_object() const {
return std::holds_alternative<Object>(data_);
}
bool JsonValue::as_bool() const {
if (!is_bool()) {
throw std::runtime_error("JSON value is not a bool");
}
return std::get<bool>(data_);
}
double JsonValue::as_number() const {
if (!is_number()) {
throw std::runtime_error("JSON value is not a number");
}
return std::get<double>(data_);
}
const std::string& JsonValue::as_string() const {
if (!is_string()) {
throw std::runtime_error("JSON value is not a string");
}
return std::get<std::string>(data_);
}
const JsonValue::Array& JsonValue::as_array() const {
if (!is_array()) {
throw std::runtime_error("JSON value is not an array");
}
return std::get<Array>(data_);
}
const JsonValue::Object& JsonValue::as_object() const {
if (!is_object()) {
throw std::runtime_error("JSON value is not an object");
}
return std::get<Object>(data_);
}
JsonValue::Array& JsonValue::as_array() {
if (!is_array()) {
throw std::runtime_error("JSON value is not an array");
}
return std::get<Array>(data_);
}
JsonValue::Object& JsonValue::as_object() {
if (!is_object()) {
throw std::runtime_error("JSON value is not an object");
}
return std::get<Object>(data_);
}
bool JsonValue::contains(const std::string& key) const {
if (!is_object()) {
return false;
}
return as_object().contains(key);
}
const JsonValue& JsonValue::at(const std::string& key) const {
return as_object().at(key);
}
namespace {
class Parser {
public:
explicit Parser(std::string_view text) : text_(text) {}
JsonValue parse() {
skip_ws();
JsonValue value = parse_value();
skip_ws();
if (pos_ != text_.size()) {
throw error("Unexpected trailing JSON content");
}
return value;
}
private:
[[nodiscard]] std::runtime_error error(const std::string& message) const {
std::ostringstream out;
out << message << " at byte " << pos_;
return std::runtime_error(out.str());
}
void skip_ws() {
while (pos_ < text_.size() && std::isspace(static_cast<unsigned char>(text_[pos_])) != 0) {
++pos_;
}
}
char peek() const {
if (pos_ >= text_.size()) {
throw error("Unexpected end of JSON");
}
return text_[pos_];
}
char consume() {
const char ch = peek();
++pos_;
return ch;
}
void expect(char expected) {
const char actual = consume();
if (actual != expected) {
std::ostringstream out;
out << "Expected '" << expected << "' but found '" << actual << "'";
throw error(out.str());
}
}
bool consume_if(char ch) {
if (pos_ < text_.size() && text_[pos_] == ch) {
++pos_;
return true;
}
return false;
}
JsonValue parse_value() {
skip_ws();
switch (peek()) {
case 'n':
parse_keyword("null");
return JsonValue(nullptr);
case 't':
parse_keyword("true");
return JsonValue(true);
case 'f':
parse_keyword("false");
return JsonValue(false);
case '"':
return JsonValue(parse_string());
case '[':
return JsonValue(parse_array());
case '{':
return JsonValue(parse_object());
default:
return JsonValue(parse_number());
}
}
void parse_keyword(std::string_view keyword) {
for (char ch : keyword) {
if (consume() != ch) {
throw error("Invalid JSON keyword");
}
}
}
std::string parse_string() {
expect('"');
std::string value;
while (true) {
if (pos_ >= text_.size()) {
throw error("Unterminated JSON string");
}
const char ch = consume();
if (ch == '"') {
return value;
}
if (ch != '\\') {
if (static_cast<unsigned char>(ch) < 0x20) {
throw error("Unescaped control character in JSON string");
}
value.push_back(ch);
continue;
}
if (pos_ >= text_.size()) {
throw error("Unterminated JSON escape");
}
const char escape = consume();
switch (escape) {
case '"':
case '\\':
case '/':
value.push_back(escape);
break;
case 'b':
value.push_back('\b');
break;
case 'f':
value.push_back('\f');
break;
case 'n':
value.push_back('\n');
break;
case 'r':
value.push_back('\r');
break;
case 't':
value.push_back('\t');
break;
case 'u': {
std::uint32_t codepoint = parse_hex_codepoint();
if (codepoint >= 0xD800 && codepoint <= 0xDBFF) {
expect('\\');
expect('u');
const std::uint32_t low = parse_hex_codepoint();
if (low < 0xDC00 || low > 0xDFFF) {
throw error("Invalid low surrogate in JSON string");
}
codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + (low - 0xDC00);
} else if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) {
throw error("Unexpected low surrogate in JSON string");
}
append_utf8(value, codepoint);
break;
}
default:
throw error("Invalid JSON escape");
}
}
}
int hex_digit(char ch) const {
if (ch >= '0' && ch <= '9') {
return ch - '0';
}
if (ch >= 'a' && ch <= 'f') {
return 10 + ch - 'a';
}
if (ch >= 'A' && ch <= 'F') {
return 10 + ch - 'A';
}
throw error("Invalid hex digit in JSON unicode escape");
}
std::uint32_t parse_hex_codepoint() {
std::uint32_t value = 0;
for (int index = 0; index < 4; ++index) {
value = (value << 4) | static_cast<std::uint32_t>(hex_digit(consume()));
}
return value;
}
void append_utf8(std::string& out, std::uint32_t codepoint) const {
if (codepoint <= 0x7F) {
out.push_back(static_cast<char>(codepoint));
return;
}
if (codepoint <= 0x7FF) {
out.push_back(static_cast<char>(0xC0 | (codepoint >> 6)));
out.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
return;
}
if (codepoint <= 0xFFFF) {
out.push_back(static_cast<char>(0xE0 | (codepoint >> 12)));
out.push_back(static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F)));
out.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
return;
}
if (codepoint <= 0x10FFFF) {
out.push_back(static_cast<char>(0xF0 | (codepoint >> 18)));
out.push_back(static_cast<char>(0x80 | ((codepoint >> 12) & 0x3F)));
out.push_back(static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F)));
out.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
return;
}
throw error("Unicode codepoint is outside the valid UTF-8 range");
}
double parse_number() {
const std::size_t start = pos_;
consume_if('-');
if (consume_if('0')) {
if (pos_ < text_.size() && std::isdigit(static_cast<unsigned char>(text_[pos_])) != 0) {
throw error("Leading zeros are not permitted in JSON numbers");
}
} else {
if (!std::isdigit(static_cast<unsigned char>(peek()))) {
throw error("Invalid JSON number");
}
while (pos_ < text_.size() && std::isdigit(static_cast<unsigned char>(text_[pos_])) != 0) {
++pos_;
}
}
if (consume_if('.')) {
if (pos_ >= text_.size() || std::isdigit(static_cast<unsigned char>(text_[pos_])) == 0) {
throw error("Invalid JSON fractional part");
}
while (pos_ < text_.size() && std::isdigit(static_cast<unsigned char>(text_[pos_])) != 0) {
++pos_;
}
}
if (consume_if('e') || consume_if('E')) {
consume_if('+') || consume_if('-');
if (pos_ >= text_.size() || std::isdigit(static_cast<unsigned char>(text_[pos_])) == 0) {
throw error("Invalid JSON exponent");
}
while (pos_ < text_.size() && std::isdigit(static_cast<unsigned char>(text_[pos_])) != 0) {
++pos_;
}
}
const std::string token(text_.substr(start, pos_ - start));
return std::stod(token);
}
JsonValue::Array parse_array() {
expect('[');
skip_ws();
JsonValue::Array values;
if (consume_if(']')) {
return values;
}
while (true) {
values.push_back(parse_value());
skip_ws();
if (consume_if(']')) {
return values;
}
expect(',');
skip_ws();
}
}
JsonValue::Object parse_object() {
expect('{');
skip_ws();
JsonValue::Object values;
if (consume_if('}')) {
return values;
}
while (true) {
skip_ws();
if (peek() != '"') {
throw error("Expected object key");
}
std::string key = parse_string();
skip_ws();
expect(':');
skip_ws();
JsonValue parsed_value = parse_value();
const auto [it, inserted] = values.emplace(key, std::move(parsed_value));
if (!inserted) {
throw error("Duplicate object key \"" + key + "\"");
}
skip_ws();
if (consume_if('}')) {
return values;
}
expect(',');
skip_ws();
}
}
std::string_view text_;
std::size_t pos_{0};
};
std::string indent_string(int level, int indent) {
return std::string(static_cast<std::size_t>(level * indent), ' ');
}
std::string escape_string(const std::string& input) {
std::ostringstream out;
out << '"';
for (unsigned char ch : input) {
switch (ch) {
case '\\':
out << "\\\\";
break;
case '"':
out << "\\\"";
break;
case '\n':
out << "\\n";
break;
case '\r':
out << "\\r";
break;
case '\t':
out << "\\t";
break;
default:
if (ch < 0x20) {
out << "\\u" << std::uppercase << std::hex << std::setw(4) << std::setfill('0')
<< static_cast<int>(ch)
<< std::nouppercase << std::dec << std::setfill(' ');
} else {
out << static_cast<char>(ch);
}
break;
}
}
out << '"';
return out.str();
}
std::string number_string(double value) {
if (!std::isfinite(value)) {
throw std::runtime_error("Cannot serialize non-finite JSON number");
}
std::ostringstream out;
out << std::setprecision(std::numeric_limits<double>::max_digits10) << value;
return out.str();
}
void serialize_impl(const JsonValue& value, int indent, int level, std::ostringstream& out) {
if (value.is_null()) {
out << "null";
return;
}
if (value.is_bool()) {
out << (value.as_bool() ? "true" : "false");
return;
}
if (value.is_number()) {
out << number_string(value.as_number());
return;
}
if (value.is_string()) {
out << escape_string(value.as_string());
return;
}
if (value.is_array()) {
const auto& array = value.as_array();
if (array.empty()) {
out << "[]";
return;
}
out << "[\n";
for (std::size_t i = 0; i < array.size(); ++i) {
out << indent_string(level + 1, indent);
serialize_impl(array[i], indent, level + 1, out);
if (i + 1 != array.size()) {
out << ',';
}
out << '\n';
}
out << indent_string(level, indent) << ']';
return;
}
const auto& object = value.as_object();
if (object.empty()) {
out << "{}";
return;
}
out << "{\n";
std::size_t index = 0;
for (const auto& [key, child] : object) {
out << indent_string(level + 1, indent) << escape_string(key) << ": ";
serialize_impl(child, indent, level + 1, out);
if (index + 1 != object.size()) {
out << ',';
}
out << '\n';
++index;
}
out << indent_string(level, indent) << '}';
}
} // namespace
JsonValue parse_json(std::string_view text) {
return Parser(text).parse();
}
std::string to_json_string(const JsonValue& value, int indent) {
std::ostringstream out;
serialize_impl(value, indent, 0, out);
out << '\n';
return out.str();
}
} // namespace mocca
+518
View File
@@ -0,0 +1,518 @@
#include "mocca/kernel.hpp"
#include "config_bridge.hpp"
#include "embedded_tables.hpp"
#include "line_codec.hpp"
#include "physics_engine.hpp"
#include <array>
#include <cmath>
#include <optional>
#include <utility>
#include <vector>
namespace mocca {
class ModernCascadeKernel::Impl {
public:
explicit Impl(const SimulationConfig& config)
: config_(config),
tables_(detail::bundled_numeric_tables()),
primitives_(tables_, config_.numerics.matrix_element_precision_digits) {}
[[nodiscard]] SimulationResult run() const {
detail::KernelState cfg = detail::build_kernel_config(tables_, config_);
primitives_.prepare_case(cfg);
auto [lyman_sum, states] = propagate_cascade(cfg);
auto lines = detail::collect_lines(cfg);
return SimulationResult{
lyman_sum,
static_cast<int>(lines.size()),
std::move(lines),
std::move(states),
cfg.warnings,
};
}
private:
[[nodiscard]] static std::array<double, 5> popj(int l1, int l2, int ll) {
std::array<double, 5> p{};
const int li = ll + 1;
const int l = std::max(l1, l2);
if (li == 1) {
p[1] = static_cast<double>(l + 1) / static_cast<double>(2 * l + 1);
p[2] = 1.0 - p[1];
return p;
}
if (li == 2) {
p[1] = static_cast<double>((l + 1) * (2 * l - 1)) / static_cast<double>(4 * l * l - 1);
p[2] = 1.0 / static_cast<double>(4 * l * l - 1);
p[3] = 1.0 - (p[1] + p[2]);
return p;
}
if (li == 3) {
if (l1 != l2) {
p[1] = static_cast<double>(l + 1) / static_cast<double>(2 * l + 1);
p[2] = 2.0 / static_cast<double>((2 * l - 3) * (2 * l + 1));
p[3] = 1.0 - (p[1] + p[2]);
return p;
}
const double d = static_cast<double>((2 * l + 1) * (2 * l + 1));
p[1] = static_cast<double>((l + 2) * (2 * l - 1)) / d;
p[2] = static_cast<double>((l - 1) * (2 * l + 3)) / d;
p[3] = 3.0 / d;
p[4] = p[3];
return p;
}
if (std::abs(l1 - l2) != 1) {
p[1] = static_cast<double>(l + 1) / static_cast<double>(2 * l + 1);
p[2] = 3.0 / static_cast<double>((2 * l - 5) * (2 * l + 1));
p[3] = 1.0 - (p[1] + p[2]);
return p;
}
const double d = static_cast<double>(4 * l * l - 1);
p[1] = static_cast<double>((2 * l - 3) * (l + 2)) / d;
p[2] = 5.0 / d;
p[3] = 6.0 / d;
p[4] = 1.0 - (p[1] + p[2] + p[3]);
return p;
}
[[nodiscard]] static double beta(int l1, int j1, int l2, int j2, int l) {
const double a1 = 0.25 * static_cast<double>(j1 * (j1 + 2));
const double a2 = 0.25 * static_cast<double>(j2 * (j2 + 2));
return (
(a2 - static_cast<double>(l2 * (l2 + 1)) + 0.750) /
(a1 - static_cast<double>(l1 * (l1 + 1)) + 0.750)) *
((a1 + a2 - static_cast<double>(l * (l + 1))) / (2.0 * a2));
}
[[nodiscard]] std::pair<double, std::vector<StateSummary>> propagate_cascade(
detail::KernelState& cfg) const {
// The kernel keeps the original one-based workspace layout to preserve
// the validated cascade-update order while expressing it in explicit
// C++ containers instead of the monolithic runner entry point.
std::vector<double> popt(4, 0.0);
const std::vector<double> pop1 = cfg.pop;
std::vector<double> pop2(7, 0.0);
popt[1] = 2.0 * cfg.pop[1];
popt[2] = 2.0 * cfg.pop[2] + 6.0 * cfg.pop[3];
popt[3] = 2.0 * cfg.pop[4] + 6.0 * cfg.pop[5] + 10.0 * cfg.pop[6];
pop2[1] = pop1[1];
pop2[2] = (popt[2] <= 1.0e-20) ? 1.0 : 1.0 / popt[2];
pop2[3] = (popt[2] <= 1.0e-20) ? 1.0 : 1.0 / popt[2];
pop2[4] = (popt[3] <= 1.0e-20) ? 1.0 : 1.0 / popt[3];
pop2[5] = (popt[3] <= 1.0e-20) ? 1.0 : 1.0 / popt[3];
pop2[6] = (popt[3] <= 1.0e-20) ? 1.0 : 1.0 / popt[3];
const int ms = 3;
const double rk = cfg.widthk / cfg.hbar;
const int nu = cfg.nmax * (cfg.nmax + 1) / 2;
std::vector<double> pnl(nu + 1, 0.0);
std::vector<double> polpos(nu + 1, 0.0);
std::vector<double> polneg(nu + 1, 0.0);
std::vector<double> width(nu + 1, 0.0);
std::vector<double> convc(nu + 1, 0.0);
std::vector<double> sporb(nu + 1, 0.0);
std::vector<double> pc0(nu + 1, 0.0);
std::vector<double> pc1(nu + 1, 0.0);
std::vector<double> pc2(nu + 1, 0.0);
std::vector<std::vector<double>> pc(ms + 1, std::vector<double>(nu + 1, 0.0));
const int mu = cfg.nmax * (cfg.nmax - 1) / 2;
for (int j = 1; j <= nu; ++j) {
if (cfg.ip8 != 0) {
pnl[j] = cfg.pln[j];
}
}
for (int j = 1; j <= cfg.nmax; ++j) {
const int jj = mu + j;
if (cfg.ip8 == 0) {
pnl[jj] = cfg.pl[j];
}
pc1[jj] = 2.0 - 2.0 * cfg.pop[1];
pc2[jj] = 2.0 * cfg.pop[1] - 1.0;
if (cfg.pop[1] <= 0.5) {
pc0[jj] = 1.0 - 2.0 * cfg.pop[1];
pc1[jj] = 2.0 * cfg.pop[1];
pc2[jj] = 0.0;
}
if (pnl[jj] > 1.0e-20) {
polpos[jj] = (1.0 + 2.0 / static_cast<double>(2 * j - 1)) / 3.0;
}
if (pnl[jj] > 1.0e-20 && j != 1) {
polneg[jj] = (1.0 - 2.0 / static_cast<double>(2 * j - 1)) / 3.0;
}
for (int shell = 1; shell <= ms; ++shell) {
pc[shell][jj] = popt[shell];
}
}
double rlyman = 0.0;
std::vector<double> zt(131, 0.0);
std::vector<std::vector<double>> zt_shell(4, std::vector<double>(131, 0.0));
std::vector<double> zr(131, 0.0);
std::vector<double> za(131, 0.0);
std::vector<double> za0(131, 0.0);
std::vector<double> za1(131, 0.0);
std::vector<double> za2(131, 0.0);
for (int i1 = 1; i1 <= cfg.nmax; ++i1) {
const int n1 = cfg.nmax + 1 - i1;
for (int i2 = 1; i2 <= n1; ++i2) {
const int l1 = i2 - 1;
double rategt = 0.0;
double rate0 = 2.0 * rk;
double rate1 = rk;
double rateauger = 0.0;
double raterd = 0.0;
const int k1 = detail::state_index(n1, l1);
if (i1 > 1) {
if (pnl[k1] < 1.0e-20) {
pc[2][k1] = popt[2];
pc[3][k1] = popt[3];
pc0[k1] = 0.0;
pc1[k1] = 2.0 - 2.0 * pop2[1];
pc2[k1] = 2.0 * pop2[1] - 1.0;
if (pop2[1] < 0.5) {
pc0[k1] = 1.0 - 2.0 * pop2[1];
pc1[k1] = 2.0 * pop2[1];
pc2[k1] = 0.0;
}
pc[1][k1] = pc1[k1] + 2.0 * pc2[k1];
} else {
const double xnorm = 1.0 / pnl[k1];
pc0[k1] *= xnorm;
pc1[k1] *= xnorm;
pc2[k1] *= xnorm;
for (int shell = 2; shell <= ms; ++shell) {
pc[shell][k1] = std::max(pc[shell][k1] * xnorm, 0.0);
}
pc[1][k1] = pc1[k1] + 2.0 * pc2[k1];
pc[1][k1] = std::min(std::max(pc[1][k1], 0.0), 2.0);
for (int shell = 1; shell <= ms; ++shell) {
if (cfg.ipc[shell] != 0) {
pc[shell][k1] = popt[shell];
}
}
if (cfg.ipc[1] != 0) {
pc2[k1] = 2.0 * pop1[1] - 1.0;
pc1[k1] = 2.0 - 2.0 * pop1[1];
pc0[k1] = 0.0;
if (pop1[1] < 0.5) {
pc2[k1] = 0.0;
pc1[k1] = 2.0 * pop1[1];
pc0[k1] = 1.0 - 2.0 * pop1[1];
}
}
polpos[k1] = polpos[k1] * xnorm * static_cast<double>(2 * l1 + 1) /
static_cast<double>(l1 + 1);
if (cfg.npol[n1] - l1 == 0 || cfg.npol[n1] - l1 == 1) {
polpos[k1] = (1.0 + 2.0 / static_cast<double>(2 * l1 + 1)) / 3.0;
}
if (l1 > 0) {
polneg[k1] = polneg[k1] * xnorm * static_cast<double>(2 * l1 + 1) /
static_cast<double>(l1);
if (cfg.npol[n1] - l1 == 0 || cfg.npol[n1] - l1 == 1) {
polneg[k1] = (1.0 - 2.0 / static_cast<double>(2 * l1 + 1)) / 3.0;
}
}
}
}
if (n1 == 1) {
continue;
}
double pc012 = pc0[k1] + pc1[k1] + pc2[k1];
if (std::abs(pc012) < 1.0e-20) {
pc012 = 1.0;
}
pc0[k1] /= pc012;
pc1[k1] /= pc012;
pc2[k1] /= pc012;
cfg.pop[1] = 0.5 * pc[1][k1];
cfg.pop[2] = pop2[2] * pc[2][k1];
cfg.pop[3] = pop2[3] * pc[2][k1];
cfg.pop[4] = pop2[4] * pc[3][k1];
cfg.pop[5] = pop2[5] * pc[3][k1];
cfg.pop[6] = pop2[6] * pc[3][k1];
std::fill(zt.begin(), zt.end(), 0.0);
for (auto& shell : zt_shell) {
std::fill(shell.begin(), shell.end(), 0.0);
}
std::fill(zr.begin(), zr.end(), 0.0);
std::fill(za.begin(), za.end(), 0.0);
std::fill(za0.begin(), za0.end(), 0.0);
std::fill(za1.begin(), za1.end(), 0.0);
std::fill(za2.begin(), za2.end(), 0.0);
int k = 0;
for (int i3 = 1; i3 <= 7; ++i3) {
const int l2 = l1 - 4 + i3;
if (l2 < 0) {
continue;
}
for (int i4 = 1; i4 <= n1; ++i4) {
const int n2 = n1 - i4 + 1;
if (n2 <= l2) {
continue;
}
if (n2 == n1 && (n2 != 2 || l1 != 0 || l2 != 1)) {
continue;
}
if (n1 == n2 && cfg.espm <= 1.0e-20) {
continue;
}
k += 1;
if (n1 == n2) {
cfg.ijk = 1;
cfg.energ = cfg.espm;
} else {
cfg.ijk = 0;
}
const double popq = cfg.pop[1];
cfg.pop[1] = 1.0;
zt[k] = primitives_.transition_rate(cfg, n1, l1, n2, l2) + 1.0e-10;
rategt += zt[k];
if (cfg.pop[1] < 1.0e-20) {
cfg.pop[1] = 1.0;
}
cfg.rsa[1] = cfg.rsa[1] / cfg.pop[1];
cfg.pop[1] = popq;
zr[k] = cfg.rad;
raterd += cfg.rad;
za[k] = cfg.rsa[1];
// `rad_to_auger` used to reconstruct the effective Auger
// width from `effective_total - radiative`, which loses
// several digits when the state is almost purely
// radiative. Accumulating the Auger contribution directly
// keeps the same physics while avoiding that cancellation.
const double transition_auger = cfg.rau + 1.0e-10;
rateauger +=
pc2[k1] * transition_auger +
pc1[k1] * (transition_auger - 0.5 * za[k]) +
pc0[k1] * (transition_auger - za[k]);
const double xr = zt[k] - cfg.rsa[1];
double t0 = xr + 0.5 * cfg.rsa[1] + rk;
double g0 = xr + 2.0 * rk + 1.0e-10;
rate1 = rate1 + t0 - rk + 1.0e-10;
rate0 = rate0 + xr + 1.0e-10;
double xm = zt[k] - za[k] * (pc0[k1] + 0.5 * pc1[k1]);
if (std::abs(xm) < 1.0e-10) {
xm = 1.0;
}
if (std::abs(t0) < 1.0e-10) {
t0 = 1.0;
}
if (std::abs(g0) < 1.0e-10) {
g0 = 1.0;
}
if (std::abs(zt[k]) < 1.0e-10) {
zt[k] = 1.0;
}
zt_shell[3][k] = pc[3][k1] - cfg.rsa[3] / xm;
if (zt_shell[3][k] < 0.0) {
zt_shell[3][k] = 0.0;
}
za2[k] = xr / zt[k] *
(pc2[k1] + rk * pc1[k1] / t0 + 2.0 * rk * rk * pc0[k1] / t0 / g0);
za1[k] = za[k] * pc2[k1] / zt[k] +
(xr + rk * za[k] / zt[k]) / t0 *
(pc1[k1] + 2.0 * rk * pc0[k1] / g0);
za0[k] = pc1[k1] * za[k] / (2.0 * t0) +
pc0[k1] / g0 * (xr + rk * za[k] / t0);
const double ym = za[k] * (
pc2[k1] / zt[k] +
pc1[k1] * (0.5 + rk / zt[k]) / t0 +
pc0[k1] * rk / t0 / g0 * (1.0 + 2.0 * rk / zt[k]));
const double xl =
rk * (pc1[k1] / t0 + 2.0 * pc0[k1] / t0 / g0 * (t0 + rk));
zt_shell[1][k] = pc[1][k1] - ym + xl;
zt_shell[2][k] = pc[2][k1] - cfg.rsa[2] / xm - xl;
if (zt_shell[1][k] < 0.0) {
zt_shell[1][k] = 0.0;
}
if (zt_shell[2][k] < 0.0) {
zt_shell[2][k] = 0.0;
}
}
}
width[k1] = (rategt * pc2[k1] + (rate1 - rk) * pc1[k1] +
(rate0 - 2.0 * rk) * pc0[k1]) *
cfg.hbar;
if (width[k1] < 0.0) {
width[k1] = 0.0;
}
if (std::abs(rate0) < 1.0e-10) {
rate0 = 1.0;
}
if (std::abs(rate1) < 1.0e-10) {
rate1 = 1.0;
}
if (std::abs(rategt) < 1.0e-10) {
rategt = 1.0;
}
convc[k1] = raterd / (rateauger + 1.0e-10);
if (convc[k1] < 0.0) {
convc[k1] = 9.999e99;
}
if (l1 != 0) {
sporb[k1] = 0.150 * std::pow(cfg.z, 4) /
static_cast<double>(n1 * n1 * n1 * l1 * (l1 + 1));
}
k = 0;
for (int i3 = 1; i3 <= 7; ++i3) {
const int l2 = l1 - 4 + i3;
if (l2 < 0) {
continue;
}
for (int i4 = 1; i4 <= n1; ++i4) {
const int n2 = n1 - i4 + 1;
if (n2 <= l2) {
continue;
}
if (n2 == n1 && (n2 != 2 || l1 != 0 || l2 != 1)) {
continue;
}
if (n2 == n1 && cfg.espm <= 1.0e-20) {
continue;
}
k += 1;
const int k2 = detail::state_index(n2, l2);
const double bnorm = pnl[k1] * (
pc2[k1] * zt[k] / rategt +
pc1[k1] *
(zt[k] - 0.5 * za[k] + rk * zt[k] / rategt) / rate1 +
pc0[k1] *
(zt[k] - za[k] + 2.0 * rk / rate1 *
(zt[k] - 0.5 * za[k] + rk * zt[k] / rategt)) /
rate0);
for (int shell = 1; shell <= ms; ++shell) {
pc[shell][k2] += zt_shell[shell][k] * bnorm;
}
pnl[k2] += bnorm;
pc2[k2] += za2[k] * bnorm;
pc1[k2] += za1[k] * bnorm;
pc0[k2] += za0[k] * bnorm;
const double radint =
pnl[k1] * zr[k] *
(pc2[k1] / rategt +
pc1[k1] * (1.0 + rk / rategt) / rate1 +
pc0[k1] *
(1.0 + 2.0 * rk / rate1 * (1.0 + rk / rategt)) /
rate0);
const int ll = std::abs(l1 - l2);
const auto p = popj(l1, l2, ll);
if (cfg.ipol == 0) {
const int li = ll + 1;
const int j1u = 2 * l1 + 1;
const int j2u = 2 * l2 + 1;
const int j1d = j1u - 2;
const int j2d = j2u - 2;
if (li <= 1) {
polpos[k2] += bnorm * p[1] * polpos[k1];
polneg[k2] += bnorm * p[2] * polneg[k1];
} else if (l2 <= l1) {
polpos[k2] +=
bnorm *
(p[1] * polpos[k1] * beta(l1, j1u, l2, j2u, ll) +
p[2] * polneg[k1] * beta(l1, j1d, l2, j2u, ll));
if (j1d != -1 && j2d != -1) {
polneg[k2] +=
bnorm * p[3] * polneg[k1] *
beta(l1, j1d, l2, j2d, ll);
}
} else {
polpos[k2] +=
bnorm * p[1] * polpos[k1] *
beta(l1, j1u, l2, j2u, ll);
polneg[k2] +=
bnorm *
(p[2] * polpos[k1] * beta(l1, j1u, l2, j2d, ll) +
p[3] * polneg[k1] * beta(l1, j1d, l2, j2d, ll));
}
}
const int li = (ll != 0) ? ll : 2;
detail::record_lines(cfg, n1, l1, n2, l2, li, radint);
if (l1 == 1 && l2 == 0 && n2 == 1) {
rlyman += radint;
}
}
}
}
}
std::vector<StateSummary> states;
states.reserve(static_cast<std::size_t>(cfg.nmax * (cfg.nmax + 1) / 2));
pc[1][1] = pc1[1] + 2.0 * pc2[1];
for (int m1 = 1; m1 <= cfg.nmax; ++m1) {
const int n1 = cfg.nmax + 1 - m1;
for (int ll1 = 1; ll1 <= n1; ++ll1) {
const int l1 = ll1 - 1;
const int k1 = detail::state_index(n1, l1);
std::optional<double> polar_down;
if (l1 > 0) {
polar_down =
-polneg[k1] * (static_cast<double>(l1) + 0.5) /
(static_cast<double>(l1) - 0.5);
}
std::optional<double> polar_up =
(cfg.ipol == 1) ? std::optional<double>{} : std::optional<double>{polpos[k1]};
std::optional<double> width_ev =
(n1 == 1 || (l1 == 0 && pnl[k1] > 1.0e-20))
? std::optional<double>{}
: std::optional<double>{width[k1]};
std::optional<double> rad_to_auger =
(n1 == 1 || (l1 == 0 && pnl[k1] > 1.0e-20))
? std::optional<double>{}
: std::optional<double>{convc[k1]};
std::optional<double> spin_orbit_ev =
(cfg.ipol == 1 || n1 == 1 || (l1 == 0 && pnl[k1] > 1.0e-20))
? std::optional<double>{}
: std::optional<double>{sporb[k1]};
states.push_back(StateSummary{
n1,
l1,
pnl[k1],
polar_up,
polar_down,
width_ev,
rad_to_auger,
spin_orbit_ev,
pc[1][k1],
pc[2][k1],
pc[3][k1],
});
}
}
for (int i = 1; i <= 6; ++i) {
cfg.pop[i] = pop1[i];
}
return {rlyman, std::move(states)};
}
SimulationConfig config_;
detail::NumericTables tables_;
detail::PhysicsEngine primitives_;
};
ModernCascadeKernel::ModernCascadeKernel(const SimulationConfig& config)
: impl_(std::make_unique<Impl>(config)) {}
ModernCascadeKernel::~ModernCascadeKernel() = default;
ModernCascadeKernel::ModernCascadeKernel(ModernCascadeKernel&&) noexcept = default;
ModernCascadeKernel& ModernCascadeKernel::operator=(ModernCascadeKernel&&) noexcept = default;
SimulationResult ModernCascadeKernel::run() const {
return impl_->run();
}
SimulationResult run_modern_kernel(const SimulationConfig& config) {
return ModernCascadeKernel(config).run();
}
} // namespace mocca
+440
View File
@@ -0,0 +1,440 @@
#include "line_codec.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <stdexcept>
#include <tuple>
namespace mocca::detail {
namespace {
constexpr int kN1Shift = 0;
constexpr int kL1Shift = 5;
constexpr int kJ1Shift = 10;
constexpr int kN2Shift = 16;
constexpr int kL2Shift = 21;
constexpr int kJ2Shift = 26;
constexpr int kMultipoleShift = 32;
constexpr std::int64_t kFiveBitMask = 0x1FLL;
constexpr std::int64_t kSixBitMask = 0x3FLL;
constexpr std::int64_t kThreeBitMask = 0x7LL;
int lower_twice_j(int l) {
return l == 0 ? 1 : 2 * l - 1;
}
int upper_twice_j(int l) {
return l == 0 ? 1 : 2 * l + 1;
}
std::int64_t pack_line_id(
int n1,
int l1,
int j1_twice,
int n2,
int l2,
int j2_twice,
int multipole) {
const auto fits_mask = [](int value, std::int64_t mask) {
return value >= 0 && static_cast<std::int64_t>(value) <= mask;
};
if (!fits_mask(n1, kFiveBitMask) || !fits_mask(l1, kFiveBitMask) ||
!fits_mask(j1_twice, kSixBitMask) || !fits_mask(n2, kFiveBitMask) ||
!fits_mask(l2, kFiveBitMask) || !fits_mask(j2_twice, kSixBitMask) ||
!fits_mask(multipole, kThreeBitMask)) {
throw std::runtime_error("Line descriptor exceeds the supported packed range");
}
return (static_cast<std::int64_t>(n1) << kN1Shift) |
(static_cast<std::int64_t>(l1) << kL1Shift) |
(static_cast<std::int64_t>(j1_twice) << kJ1Shift) |
(static_cast<std::int64_t>(n2) << kN2Shift) |
(static_cast<std::int64_t>(l2) << kL2Shift) |
(static_cast<std::int64_t>(j2_twice) << kJ2Shift) |
(static_cast<std::int64_t>(multipole) << kMultipoleShift);
}
void store_line(
KernelState& cfg,
int index,
double energy,
int n1,
int l1,
int j1_twice,
int n2,
int l2,
int j2_twice,
int multipole,
double intensity) {
cfg.line_e[index] = energy;
cfg.line_ia[index] = pack_line_id(n1, l1, j1_twice, n2, l2, j2_twice, multipole);
cfg.line_ai[index] = intensity;
}
std::string multipole_name(int multipole) {
switch (multipole) {
case 1:
return "DIP";
case 2:
return "QUA";
case 3:
return "OCT";
default:
throw std::runtime_error("Invalid packed multipole id");
}
}
} // namespace
void record_lines(
KernelState& cfg,
int n1,
int l1,
int n2,
int l2,
int multipole,
double intensity) {
if (intensity <= cfg.climit) {
return;
}
if (cfg.m > 995) {
const std::string warning =
"*** ATTENTION *** OVERFLOWING CAPABILITY OF SORTING *** PLEASE RESTRICT CRITERIA FOR LINES TO PASS ***";
if (std::find(cfg.warnings.begin(), cfg.warnings.end(), warning) == cfg.warnings.end()) {
cfg.warnings.push_back(warning);
}
return;
}
double e12 = 0.0;
double e22 = 0.0;
double e11 = cfg.energy[n1][2 * l1 + 1];
if (l1 != 0) {
e12 = cfg.energy[n1][2 * l1];
}
double e21 = cfg.energy[n2][2 * l2 + 1];
if (l2 != 0) {
e22 = cfg.energy[n2][2 * l2];
}
if (n1 != n2 && (e21 - e11 <= cfg.elow || e21 - e11 >= cfg.ehigh)) {
return;
}
if (n1 == n2) {
e11 = 0.0;
const double ez = 0.5 * (cfg.energy[2][2] - cfg.energy[2][3]);
e21 = cfg.esp * 1.0e-6 - ez;
e22 = e21 + 2.0 * ez;
}
if (multipole == 1) {
const int ll = std::max(l1, l2);
const double an = intensity / static_cast<double>(4 * ll * ll - 1);
store_line(
cfg,
cfg.m,
e21 - e11,
n1,
l1,
upper_twice_j(l1),
n2,
l2,
upper_twice_j(l2),
multipole,
an * static_cast<double>((ll + 1) * (2 * ll - 1)));
store_line(
cfg,
cfg.m + 1,
l1 == ll ? e21 - e12 : e22 - e11,
n1,
l1,
l1 == ll ? lower_twice_j(l1) : upper_twice_j(l1),
n2,
l2,
l1 == ll ? upper_twice_j(l2) : lower_twice_j(l2),
multipole,
an);
store_line(
cfg,
cfg.m + 2,
e22 - e12,
n1,
l1,
lower_twice_j(l1),
n2,
l2,
lower_twice_j(l2),
multipole,
an * static_cast<double>((ll - 1) * (2 * ll + 1)));
cfg.m += 3;
if (ll == 1) {
cfg.m -= 1;
}
return;
}
if (multipole == 2) {
if (l1 != l2) {
const int ll = std::max(l1, l2);
const double an = intensity / static_cast<double>((2 * ll - 3) * (2 * ll + 1));
store_line(
cfg,
cfg.m,
e21 - e11,
n1,
l1,
upper_twice_j(l1),
n2,
l2,
upper_twice_j(l2),
multipole,
an * static_cast<double>((ll + 1) * (2 * ll - 3)));
store_line(
cfg,
cfg.m + 1,
l1 == ll ? e21 - e12 : e22 - e11,
n1,
l1,
l1 == ll ? lower_twice_j(l1) : upper_twice_j(l1),
n2,
l2,
l1 == ll ? upper_twice_j(l2) : lower_twice_j(l2),
multipole,
2.0 * an);
store_line(
cfg,
cfg.m + 2,
e22 - e12,
n1,
l1,
lower_twice_j(l1),
n2,
l2,
lower_twice_j(l2),
multipole,
an * static_cast<double>((ll - 2) * (2 * ll + 1)));
cfg.m += 3;
if (ll <= 2) {
cfg.m -= 1;
}
return;
}
if (l1 == 0) {
return;
}
const double an = intensity / static_cast<double>((2 * l1 + 1) * (2 * l1 + 1));
store_line(
cfg,
cfg.m,
e21 - e11,
n1,
l1,
upper_twice_j(l1),
n2,
l2,
upper_twice_j(l2),
multipole,
an * static_cast<double>((l1 + 2) * (2 * l1 - 1)));
store_line(
cfg,
cfg.m + 1,
e22 - e11,
n1,
l1,
upper_twice_j(l1),
n2,
l2,
lower_twice_j(l2),
multipole,
3.0 * an);
store_line(
cfg,
cfg.m + 2,
e21 - e12,
n1,
l1,
lower_twice_j(l1),
n2,
l2,
upper_twice_j(l2),
multipole,
3.0 * an);
store_line(
cfg,
cfg.m + 3,
e22 - e12,
n1,
l1,
lower_twice_j(l1),
n2,
l2,
lower_twice_j(l2),
multipole,
an * static_cast<double>((l1 - 1) * (2 * l1 + 3)));
cfg.m += 4;
if (l1 == 1) {
cfg.m -= 1;
}
return;
}
if (std::abs(l1 - l2) != 1) {
const int ll = std::max(l1, l2);
const double an = intensity / static_cast<double>((2 * ll - 5) * (2 * ll + 1));
store_line(
cfg,
cfg.m,
e21 - e11,
n1,
l1,
upper_twice_j(l1),
n2,
l2,
upper_twice_j(l2),
multipole,
an * static_cast<double>((2 * ll - 5) * (ll + 1)));
store_line(
cfg,
cfg.m + 1,
l1 == ll ? e21 - e12 : e22 - e11,
n1,
l1,
l1 == ll ? lower_twice_j(l1) : upper_twice_j(l1),
n2,
l2,
l1 == ll ? upper_twice_j(l2) : lower_twice_j(l2),
multipole,
3.0 * an);
store_line(
cfg,
cfg.m + 2,
e22 - e12,
n1,
l1,
lower_twice_j(l1),
n2,
l2,
lower_twice_j(l2),
multipole,
an * static_cast<double>((2 * ll + 1) * (ll - 3)));
cfg.m += 3;
if (ll <= 3) {
cfg.m -= 1;
}
return;
}
const int ll = std::max(l1, l2);
const double an = intensity / static_cast<double>(4 * ll * ll - 1);
store_line(
cfg,
cfg.m,
e21 - e11,
n1,
l1,
upper_twice_j(l1),
n2,
l2,
upper_twice_j(l2),
multipole,
an * static_cast<double>((2 * ll - 3) * (ll + 2)));
store_line(
cfg,
cfg.m + 1,
e22 - e11,
n1,
l1,
upper_twice_j(l1),
n2,
l2,
lower_twice_j(l2),
multipole,
5.0 * an);
store_line(
cfg,
cfg.m + 2,
e21 - e12,
n1,
l1,
lower_twice_j(l1),
n2,
l2,
upper_twice_j(l2),
multipole,
6.0 * an);
store_line(
cfg,
cfg.m + 3,
e22 - e12,
n1,
l1,
lower_twice_j(l1),
n2,
l2,
lower_twice_j(l2),
multipole,
an * static_cast<double>((2 * ll + 3) * (ll - 2)));
cfg.m += 4;
if (ll <= 2) {
cfg.m -= 1;
}
}
std::vector<TransitionLine> collect_lines(const KernelState& cfg) {
std::vector<TransitionLine> lines;
lines.reserve(cfg.m > 1 ? static_cast<std::size_t>(cfg.m - 1) : 0);
for (int i = 1; i < cfg.m; ++i) {
if (!std::isfinite(cfg.line_ai[i]) || !std::isfinite(cfg.line_e[i])) {
continue;
}
if (cfg.line_ai[i] <= cfg.climit) {
continue;
}
const std::int64_t ia0 = cfg.line_ia[i];
const int n1 = static_cast<int>((ia0 >> kN1Shift) & kFiveBitMask);
const int l1 = static_cast<int>((ia0 >> kL1Shift) & kFiveBitMask);
const int j1 = static_cast<int>((ia0 >> kJ1Shift) & kSixBitMask);
const int n2 = static_cast<int>((ia0 >> kN2Shift) & kFiveBitMask);
const int l2 = static_cast<int>((ia0 >> kL2Shift) & kFiveBitMask);
const int j2 = static_cast<int>((ia0 >> kJ2Shift) & kSixBitMask);
const int multipole_id = static_cast<int>((ia0 >> kMultipoleShift) & kThreeBitMask);
lines.push_back(TransitionLine{
n1,
l1,
j1,
n2,
l2,
j2,
multipole_name(multipole_id),
1000.0 * cfg.line_e[i],
cfg.line_ai[i],
});
}
std::sort(lines.begin(), lines.end(), [](const TransitionLine& lhs, const TransitionLine& rhs) {
return std::tie(
lhs.energy_kev,
lhs.n1,
lhs.l1,
lhs.j1_twice,
lhs.n2,
lhs.l2,
lhs.j2_twice,
lhs.multipole,
lhs.intensity) <
std::tie(
rhs.energy_kev,
rhs.n1,
rhs.l1,
rhs.j1_twice,
rhs.n2,
rhs.l2,
rhs.j2_twice,
rhs.multipole,
rhs.intensity);
});
return lines;
}
} // namespace mocca::detail
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "mocca/api.hpp"
#include "physics_engine.hpp"
#include <vector>
namespace mocca::detail {
void record_lines(
KernelState& cfg,
int n1,
int l1,
int n2,
int l2,
int multipole,
double intensity);
[[nodiscard]] std::vector<TransitionLine> collect_lines(const KernelState& cfg);
} // namespace mocca::detail
File diff suppressed because it is too large Load Diff
+228
View File
@@ -0,0 +1,228 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <limits>
#include <string>
#include <unordered_map>
#include <vector>
#include "wide_float.hpp"
namespace mocca::detail {
using NumericTables = std::unordered_map<std::string, std::vector<double>>;
struct KernelState {
explicit KernelState(const NumericTables& numeric_tables);
std::vector<std::string> warnings;
int ijk{0};
double energ{0.0};
double econs{5.505355e-03};
double econst{0.0};
double d2p1sm{0.0};
double d2p1s{0.0};
std::vector<double> bem;
std::vector<double> zsa;
std::vector<double> be;
int k0{3};
int k1{4};
int k2{4};
int k3{4};
std::vector<int> nn0;
std::vector<int> nn1;
std::vector<int> nn2;
std::vector<int> nn3;
std::vector<double> r0;
std::vector<double> r1;
std::vector<double> r2;
std::vector<double> r3;
std::vector<int> ip1;
std::vector<int> ip2;
std::vector<int> ip3;
std::vector<int> iq1;
std::vector<int> iq2;
std::vector<int> iq3;
std::vector<double> f;
double fd{15.0};
double pi{3.1415926535};
double picoef{1.298778e17};
double amassm{206.7686};
std::vector<int> ne;
std::vector<int> jk;
double coeff{4.134139e16};
double alfa{7.297353e-03};
double amasse{511003.4};
double z{0.0};
double zsk{0.0};
double zsl{0.0};
double zsm{0.0};
double zskz{0.0};
double zslz{0.0};
double zsmz{0.0};
std::vector<double> amzz;
std::vector<double> expmon;
std::vector<double> expdip;
std::vector<double> expqua;
std::vector<double> expoct;
std::vector<double> coemon;
std::vector<double> coedip;
std::vector<double> coequa;
std::vector<double> coeoct;
std::vector<int> ifm;
std::vector<int> ifd;
std::vector<int> ifq;
std::vector<int> ifo;
double angd{0.0};
double angq{0.0};
double ango{0.0};
double aid{0.0};
double aiq{0.0};
double aio{0.0};
double aidsq{0.0};
double aiqsq{0.0};
double aiosq{0.0};
std::vector<double> coedp;
std::vector<double> coeq;
std::vector<double> coeo;
std::vector<double> coed;
std::vector<int> ll;
std::vector<int> m1;
std::vector<int> m2;
std::vector<int> m3;
std::vector<double> yc;
int idb{0};
int irr{0};
std::vector<double> rr;
double rau{0.0};
double rad{0.0};
std::vector<double> ra;
std::vector<double> rd;
std::vector<double> rsa;
std::vector<double> pop;
std::vector<int> jtm;
std::vector<int> jtd;
std::vector<int> jtq;
std::vector<int> jto;
std::vector<int> jm;
std::vector<int> jd;
std::vector<int> jq;
std::vector<int> jo;
int iyc{0};
std::vector<int> ij;
std::vector<double> yj;
std::vector<int> jj1;
double ehigh{20.0};
double elow{0.040};
double climit{1.0e-06};
double eres{0.000300};
double esp{0.0};
double espm{0.0};
int m{1};
std::vector<double> line_e;
std::vector<double> line_ai;
std::vector<std::int64_t> line_ia;
std::vector<std::vector<double>> energy;
double dza{0.0};
double dza2{0.0};
double dredm{0.0};
int icc{0};
std::vector<double> cd;
double ea{99.0};
double eb{99.0};
int idr{0};
double zmk{2.0};
double zml{4.0};
double zmm{9.0};
double zmkm{4.0};
double zmlm{8.0};
double zmmm{18.0};
int ivers{0};
std::vector<double> pl;
std::vector<int> npol;
int ipol{0};
double cl1{0.0};
double cl2{0.0};
int ide{10000};
std::vector<double> pln;
int ip8{0};
double a{140.0};
double cfm{0.0};
double tfm{2.3001};
double step{0.0};
double rmatch{0.0};
double widthk{0.0};
std::vector<int> ipc;
int nopt{0};
int nmax{15};
double alexp{0.0};
double amassa{0.0};
double amassn{931.48};
double hbar{6.582173e-16};
};
[[nodiscard]] NumericTables load_numeric_tables(const std::filesystem::path& fortran_path);
[[nodiscard]] int state_index(int n, int l);
class PhysicsEngine {
public:
explicit PhysicsEngine(const NumericTables& numeric_tables, int precision_digits = 120);
void prepare_case(KernelState& cfg) const;
[[nodiscard]] double transition_rate(KernelState& cfg, int n1, int l1, int n2, int l2) const;
private:
[[nodiscard]] const std::vector<WideFloat>& factor_table(double fd) const;
[[nodiscard]] double powi(double base, int exponent) const;
[[nodiscard]] double continuum_extpy(double y) const;
[[nodiscard]] double continuum_expiy(double y) const;
[[nodiscard]] double rmon_coeff(const KernelState& cfg, double y) const;
[[nodiscard]] double point(const KernelState& cfg, int n, int j) const;
[[nodiscard]] double matel(KernelState& cfg, int n1, int l1, int n2, int l2, int l, double a, int n) const;
[[nodiscard]] double matelu(KernelState& cfg, int n1, int l1, int n2, int l2, int l) const;
[[nodiscard]] double rdipu(KernelState& cfg, int n1, int l1, int n2, int l2, int n, double enem, double y, int mm) const;
[[nodiscard]] double rquau(KernelState& cfg, int n1, int l1, int n2, int l2, int n, double enem, double y, int m) const;
[[nodiscard]] double roctu(KernelState& cfg, int n1, int l1, int n2, int l2, int n, double enem, double y, int m) const;
[[nodiscard]] double rmon(KernelState& cfg, int n1, int l1, int n2, int l2, int n, double y) const;
[[nodiscard]] double rdip(KernelState& cfg, int n1, int l1, int n2, int l2, int n, int m, double y) const;
[[nodiscard]] double rqua(KernelState& cfg, int n1, int l1, int n2, int l2, int n, int m, double y) const;
[[nodiscard]] double roct(KernelState& cfg, int n1, int l1, int n2, int l2, int n, int m, double y) const;
void warn(KernelState& cfg, const std::string& message) const;
void apply_input_energy_defaults(KernelState& cfg) const;
void ffix(KernelState& cfg) const;
const NumericTables& tables_;
double default_amassm_{206.7686};
int precision_digits_;
unsigned long precision_bits_;
};
} // namespace mocca::detail
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include <cmath>
#include <string>
namespace mocca::detail {
class WideFloat {
public:
WideFloat() = default;
WideFloat(int value) : value_(static_cast<long double>(value)) {}
WideFloat(long value) : value_(static_cast<long double>(value)) {}
WideFloat(double value) : value_(static_cast<long double>(value)) {}
WideFloat(long double value) : value_(value) {}
WideFloat(int value, unsigned long) : value_(static_cast<long double>(value)) {}
WideFloat(long value, unsigned long) : value_(static_cast<long double>(value)) {}
WideFloat(double value, unsigned long) : value_(static_cast<long double>(value)) {}
WideFloat(long double value, unsigned long) : value_(value) {}
WideFloat(const std::string& value, unsigned long) : value_(std::stold(value)) {}
[[nodiscard]] double get_d() const {
return static_cast<double>(value_);
}
[[nodiscard]] long double raw() const {
return value_;
}
WideFloat& operator+=(const WideFloat& other) {
value_ += other.value_;
return *this;
}
WideFloat& operator-=(const WideFloat& other) {
value_ -= other.value_;
return *this;
}
WideFloat& operator*=(const WideFloat& other) {
value_ *= other.value_;
return *this;
}
WideFloat& operator/=(const WideFloat& other) {
value_ /= other.value_;
return *this;
}
friend WideFloat operator+(WideFloat lhs, const WideFloat& rhs) {
lhs += rhs;
return lhs;
}
friend WideFloat operator-(WideFloat lhs, const WideFloat& rhs) {
lhs -= rhs;
return lhs;
}
friend WideFloat operator*(WideFloat lhs, const WideFloat& rhs) {
lhs *= rhs;
return lhs;
}
friend WideFloat operator/(WideFloat lhs, const WideFloat& rhs) {
lhs /= rhs;
return lhs;
}
friend bool operator<(const WideFloat& lhs, const WideFloat& rhs) {
return lhs.value_ < rhs.value_;
}
private:
long double value_{0.0L};
};
inline WideFloat abs(const WideFloat& value) {
return std::fabsl(value.raw());
}
inline WideFloat sqrt(const WideFloat& value) {
return std::sqrt(value.raw());
}
} // namespace mocca::detail
+455
View File
@@ -0,0 +1,455 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
def one_based(values: list[float] | list[int], size: int, fill: float | int = 0) -> list[float] | list[int]:
padded = list(values)
if len(padded) < size:
padded.extend([fill] * (size - len(padded)))
return [fill] + padded
def new_energy_table() -> list[list[float]]:
return [[0.0 for _ in range(41)] for _ in range(21)]
def state_index(n: int, l: int) -> int:
return n * (n - 1) // 2 + l + 1
@dataclass
class DeckConfig:
amassa: float = 0.0
amassn: float = 931.48
d2p1s: float = 0.0
be: list[float] = field(default_factory=lambda: one_based([0.0, 0.0, 0.0], 3, 0.0))
k0: int = 3
k1: int = 4
k2: int = 4
k3: int = 4
nn0: list[int] = field(default_factory=lambda: one_based([1, 2, 3], 3, 0))
nn1: list[int] = field(default_factory=lambda: one_based([0, 1, 2, 3, 3, 3, 3], 7, 0))
nn2: list[int] = field(default_factory=lambda: one_based([0, 1, 2, 3, 3, 3, 3], 7, 0))
nn3: list[int] = field(default_factory=lambda: one_based([0, 1, 2, 3, 3, 3, 3], 7, 0))
ip1: list[int] = field(default_factory=lambda: one_based([0, 1, 1, 1, 1, 1, 1], 7, 0))
ip2: list[int] = field(default_factory=lambda: one_based([0, 1, 1, 1, 1, 1, 1], 7, 0))
ip3: list[int] = field(default_factory=lambda: one_based([0, 1, 1, 1, 1, 1, 1], 7, 0))
iq1: list[int] = field(default_factory=lambda: one_based([0] * 7, 7, 0))
iq2: list[int] = field(default_factory=lambda: one_based([0] * 7, 7, 0))
iq3: list[int] = field(default_factory=lambda: one_based([0] * 7, 7, 0))
fd: float = 15.0
amassm: float = 206.7686
amasse: float = 511003.4
z: float = 0.0
zsk: float = 0.0
zsl: float = 0.0
zsm: float = 0.0
m1: list[int] = field(default_factory=lambda: one_based([0, 0, 0, 0, 1, 2, 3], 7, 0))
m2: list[int] = field(default_factory=lambda: one_based([0, 0, 0, 0, 1, 2, 3], 7, 0))
m3: list[int] = field(default_factory=lambda: one_based([0, 0, 0, 0, 1, 2, 3], 7, 0))
pop: list[float] = field(default_factory=lambda: one_based([1.0] * 6, 6, 0.0))
ehigh: float = 20.0
elow: float = 0.040
climit: float = 1.0e-06
eres: float = 0.000300
ea: float = 99.0
eb: float = 99.0
a: float = 140.0
cfm: float = 0.0
tfm: float = 2.3001
step: float = 0.0
rmatch: float = 0.0
widthk: float = 0.0
nopt: int = 0
nmax: int = 15
alexp: float = 0.0
energy: list[list[float]] = field(default_factory=new_energy_table)
npol: list[int] = field(default_factory=lambda: one_based([-1] * 20, 20, 0))
ipol: int = 0
idb: int = 0
yc: list[float] = field(default_factory=lambda: one_based([1.0, 1.0, 1.0, 1.0], 4, 0.0))
ipc: list[int] = field(default_factory=lambda: one_based([0, 0, 0], 3, 0))
cl1: float = 0.0
cl2: float = 0.0
esp: float = 0.0
ide: int = 10000
jtm: list[int] = field(default_factory=lambda: one_based([1] * 6, 6, 0))
jtd: list[int] = field(default_factory=lambda: one_based([1] * 6, 6, 0))
jtq: list[int] = field(default_factory=lambda: one_based([1] * 6, 6, 0))
jto: list[int] = field(default_factory=lambda: one_based([1] * 6, 6, 0))
pl: list[float] = field(default_factory=lambda: one_based([0.0] * 20, 20, 0.0))
pln: list[float] = field(default_factory=lambda: one_based([0.0] * 210, 210, 0.0))
ip8: int = 0
warnings: list[str] = field(default_factory=list)
NUMBER_PATTERN = re.compile(r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[EeDd][+-]?\d+)?")
def parse_numbers(payload: str) -> list[float]:
payload = payload.split(";", 1)[0]
values = [token.replace("D", "E").replace("d", "E") for token in NUMBER_PATTERN.findall(payload)]
return [float(value) for value in values]
def parse_ints(payload: str) -> list[int]:
return [int(round(value)) for value in parse_numbers(payload)]
def apply_card(cfg: DeckConfig, code: str, payload: str) -> str | None:
code = code.strip().upper()
if not code:
return None
if code == "STO":
return "stop"
if code == "XEQ":
return "execute"
if code == "AMA":
values = parse_numbers(payload)
if values:
cfg.amassa = values[0]
elif code == "AMN":
values = parse_numbers(payload)
if values:
cfg.amassn = values[0]
elif code == "D21":
values = parse_numbers(payload)
if values:
cfg.d2p1s = values[0]
elif code == "BE":
for index, value in enumerate(parse_numbers(payload)[:3], start=1):
cfg.be[index] = value
elif code == "K":
values = parse_ints(payload)
if len(values) >= 4:
cfg.k0, cfg.k1, cfg.k2, cfg.k3 = values[:4]
elif code == "NN0":
for index, value in enumerate(parse_ints(payload)[: cfg.k0], start=1):
cfg.nn0[index] = value
elif code == "NN1":
for index, value in enumerate(parse_ints(payload)[: cfg.k1], start=1):
cfg.nn1[index] = value
elif code == "NN2":
for index, value in enumerate(parse_ints(payload)[: cfg.k2], start=1):
cfg.nn2[index] = value
elif code == "NN3":
for index, value in enumerate(parse_ints(payload)[: cfg.k3], start=1):
cfg.nn3[index] = value
elif code == "IP1":
for index, value in enumerate(parse_ints(payload)[: cfg.k1], start=1):
cfg.ip1[index] = value
elif code == "IP2":
for index, value in enumerate(parse_ints(payload)[: cfg.k2], start=1):
cfg.ip2[index] = value
elif code == "IP3":
for index, value in enumerate(parse_ints(payload)[: cfg.k3], start=1):
cfg.ip3[index] = value
elif code == "FD":
values = parse_numbers(payload)
if values:
cfg.fd = values[0]
elif code == "AMM":
values = parse_numbers(payload)
if values:
cfg.amassm = values[0]
elif code == "AME":
values = parse_numbers(payload)
if values:
cfg.amasse = values[0]
elif code == "Z":
values = parse_numbers(payload)
if values:
cfg.z = values[0]
elif code == "ZS":
values = parse_numbers(payload)
if len(values) >= 3:
cfg.zsk, cfg.zsl, cfg.zsm = values[:3]
elif code == "M1":
for index, value in enumerate(parse_ints(payload)[: cfg.k1], start=1):
cfg.m1[index] = value
elif code == "M2":
for index, value in enumerate(parse_ints(payload)[: cfg.k2], start=1):
cfg.m2[index] = value
elif code == "M3":
for index, value in enumerate(parse_ints(payload)[: cfg.k3], start=1):
cfg.m3[index] = value
elif code == "POP":
for index, value in enumerate(parse_numbers(payload)[:6], start=1):
cfg.pop[index] = min(max(value, 0.0), 1.0)
elif code == "EHI":
values = parse_numbers(payload)
if values:
cfg.ehigh = values[0]
elif code == "ELO":
values = parse_numbers(payload)
if values:
cfg.elow = values[0]
elif code == "CLM":
values = parse_numbers(payload)
if values:
cfg.climit = values[0]
elif code == "ERS":
values = parse_numbers(payload)
if values:
cfg.eres = values[0]
elif code == "EAB":
values = parse_numbers(payload)
if len(values) >= 2:
cfg.ea, cfg.eb = values[:2]
elif code == "A":
values = parse_numbers(payload)
if values:
cfg.a = values[0]
elif code == "CT":
values = parse_numbers(payload)
if len(values) >= 2:
cfg.cfm, cfg.tfm = values[:2]
elif code == "STP":
values = parse_numbers(payload)
if len(values) >= 2:
cfg.step, cfg.rmatch = values[:2]
elif code == "KWD":
values = parse_numbers(payload)
if values:
cfg.widthk = values[0]
elif code == "NOP":
values = parse_ints(payload)
if values:
cfg.nopt = values[0]
elif code == "NMX":
values = parse_numbers(payload)
if values:
cfg.nmax = int(round(values[0]))
if len(values) >= 2 and cfg.nopt < 1:
cfg.alexp = values[1]
elif code == "PL":
values = parse_numbers(payload)
if len(values) >= 2:
level = int(round(values[0]))
if 0 <= level <= 19:
cfg.pl[level + 1] = values[1]
elif code == "DIR":
values = parse_numbers(payload)
if len(values) >= 4:
nstate = int(round(values[0]))
kappa = int(round(values[1]))
vacuum = values[2]
binding = values[3]
if vacuum < 0.0:
binding = binding + vacuum
cfg.energy[nstate][kappa] = binding + vacuum
elif code == "NPL":
for index, value in enumerate(parse_ints(payload)[:20], start=1):
cfg.npol[index] = value
elif code == "IPL":
values = parse_ints(payload)
if values:
cfg.ipol = values[0]
elif code == "IDB":
values = parse_ints(payload)
if values:
cfg.idb = values[0]
elif code == "YC":
for index, value in enumerate(parse_numbers(payload)[:4], start=1):
cfg.yc[index] = value
elif code == "IPC":
for index, value in enumerate(parse_ints(payload)[:3], start=1):
cfg.ipc[index] = value
elif code == "CL":
values = parse_numbers(payload)
if len(values) >= 2:
cfg.cl1, cfg.cl2 = values[:2]
elif code == "ESP":
values = parse_numbers(payload)
if values:
cfg.esp = values[0]
elif code == "IDE":
values = parse_ints(payload)
if values:
cfg.ide = values[0]
elif code == "JTM":
for index, value in enumerate(parse_ints(payload)[:6], start=1):
cfg.jtm[index] = value
elif code == "JTD":
for index, value in enumerate(parse_ints(payload)[:6], start=1):
cfg.jtd[index] = value
elif code == "JTQ":
for index, value in enumerate(parse_ints(payload)[:6], start=1):
cfg.jtq[index] = value
elif code == "JTO":
for index, value in enumerate(parse_ints(payload)[:6], start=1):
cfg.jto[index] = value
elif code == "PLN":
values = parse_numbers(payload)
if len(values) >= 4:
n8 = int(round(values[0]))
ld8 = int(round(values[1]))
lu8 = int(round(values[2]))
a8 = values[3]
entries = values[4:]
count = lu8 - ld8 + 1
total = 0.0
for offset in range(count):
if offset >= len(entries):
break
index = state_index(n8, ld8 + offset)
cfg.pln[index] = entries[offset]
total += entries[offset]
scale = a8 if a8 > 0.0 else total
normalizer = max(total, 1.0e-20)
for offset in range(count):
if offset >= len(entries):
break
index = state_index(n8, ld8 + offset)
cfg.pln[index] = cfg.pln[index] * scale / normalizer
elif code == "IP":
values = parse_ints(payload)
if values:
cfg.ip8 = values[0]
else:
cfg.warnings.append(f"Ignored unsupported card {code}")
return None
def parse_first_case(deck_text: str) -> DeckConfig:
cfg = DeckConfig()
for raw_line in deck_text.splitlines():
if raw_line.strip() == "":
continue
code = raw_line[:3]
payload = raw_line[10:] if len(raw_line) > 10 else ""
action = apply_card(cfg, code, payload)
if action == "execute":
return cfg
if action == "stop":
break
raise SystemExit("No XEQ card found in the input deck.")
def capture_config(cfg: DeckConfig) -> dict[str, object]:
if cfg.ip8 != 0:
weights: list[dict[str, object]] = []
for n in range(1, cfg.nmax + 1):
for l in range(n):
weight = cfg.pln[state_index(n, l)]
if abs(weight) > 0.0:
weights.append({"n": n, "l": l, "weight": weight})
return {"mode": "explicit_nl", "n_max": cfg.nmax, "nl_weights": weights}
if cfg.nopt == 0:
return {"mode": "statistical_l", "n_max": cfg.nmax, "alpha": cfg.alexp}
if cfg.nopt == 2:
return {
"mode": "quadratic_l",
"n_max": cfg.nmax,
"quadratic_coefficients": [cfg.cl1, cfg.cl2],
}
if cfg.nopt == 4:
return {"mode": "legacy_empty", "n_max": cfg.nmax}
return {
"mode": "explicit_l",
"n_max": cfg.nmax,
"l_weights": [cfg.pl[index] for index in range(1, cfg.nmax + 1)],
}
def dirac_energies(cfg: DeckConfig) -> list[dict[str, object]]:
entries: list[dict[str, object]] = []
for n in range(1, min(cfg.nmax, len(cfg.energy) - 1) + 1):
for kappa in range(1, len(cfg.energy[n])):
value = cfg.energy[n][kappa]
if value > 0.0:
entries.append(
{
"n": n,
"kappa": kappa,
"vacuum_polarization_kev": 0.0,
"binding_kev": value,
}
)
return entries
def convert(deck_path: Path, case_name: str | None) -> dict[str, object]:
cfg = parse_first_case(deck_path.read_text())
payload: dict[str, object] = {
"schema_version": 1,
"metadata": {"case_name": case_name or deck_path.stem},
"atom": {
"atomic_number": cfg.z,
"effective_shell_charges": [cfg.zsk, cfg.zsl, cfg.zsm],
"binding_energies_ev": [cfg.be[1], cfg.be[2], cfg.be[3]],
"atomic_mass": cfg.a,
"exact_mass_number": cfg.amassa if cfg.amassa > 0.0 else None,
},
"masses": {
"muon_electron_masses": cfg.amassm,
"electron_mass_ev": cfg.amasse,
"nucleon_mass_mev": cfg.amassn,
},
"transitions": {
"two_p_to_one_s_energy_ev": cfg.d2p1s if cfg.d2p1s > 0.0 else None,
"two_s_to_two_p_split_ev": cfg.esp if cfg.esp > 0.0 else None,
"dirac_energies": dirac_energies(cfg),
},
"capture": capture_config(cfg),
"channels": {
"case_counts": [cfg.k0, cfg.k1, cfg.k2, cfg.k3],
"monopole_shells": [cfg.nn0[index] for index in range(1, cfg.k0 + 1)],
"dipole_shells": [cfg.nn1[index] for index in range(1, cfg.k1 + 1)],
"quadrupole_shells": [cfg.nn2[index] for index in range(1, cfg.k2 + 1)],
"octupole_shells": [cfg.nn3[index] for index in range(1, cfg.k3 + 1)],
"dipole_subshell_channels": [cfg.m1[index] for index in range(1, cfg.k1 + 1)],
"quadrupole_subshell_channels": [cfg.m2[index] for index in range(1, cfg.k2 + 1)],
"octupole_subshell_channels": [cfg.m3[index] for index in range(1, cfg.k3 + 1)],
"dipole_penetration_codes": [cfg.ip1[index] for index in range(1, cfg.k1 + 1)],
"quadrupole_penetration_codes": [cfg.ip2[index] for index in range(1, cfg.k2 + 1)],
"octupole_penetration_codes": [cfg.ip3[index] for index in range(1, cfg.k3 + 1)],
"dipole_penetration_avg_n_cutoffs": [cfg.iq1[index] for index in range(1, cfg.k1 + 1)],
"quadrupole_penetration_avg_n_cutoffs": [cfg.iq2[index] for index in range(1, cfg.k2 + 1)],
"octupole_penetration_avg_n_cutoffs": [cfg.iq3[index] for index in range(1, cfg.k3 + 1)],
},
"shell_model": {
"subshell_populations": [cfg.pop[index] for index in range(1, 7)],
"refill_codes": [cfg.ipc[index] for index in range(1, 4)],
"penetration_cutoffs": [cfg.yc[index] for index in range(1, 5)],
"width_k_ev": cfg.widthk,
"track_polarization": cfg.ipol == 0,
},
"reporting": {
"line_energy_min_mev": cfg.elow,
"line_energy_max_mev": cfg.ehigh,
"line_intensity_threshold": cfg.climit,
"energy_resolution_mev": cfg.eres,
},
"model": {"factorial_divider": cfg.fd},
"numerics": {"matrix_element_precision_digits": 120},
}
if cfg.warnings:
payload["translator_warnings"] = cfg.warnings
return payload
def main() -> None:
parser = argparse.ArgumentParser(description="Translate a legacy cascade card deck into the MOCCA JSON input format.")
parser.add_argument("deck", type=Path, help="Legacy input deck")
parser.add_argument("--output", type=Path, help="Output JSON path. Defaults to stdout.")
parser.add_argument("--case-name", help="Optional case name override")
args = parser.parse_args()
payload = json.dumps(convert(args.deck, args.case_name), indent=2) + "\n"
if args.output is None:
print(payload, end="")
else:
args.output.write_text(payload)
if __name__ == "__main__":
main()