diff --git a/CMakeLists.txt b/CMakeLists.txt index 02dee5699..9d35589fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,7 @@ set(HERMES_SOURCES src/evolve_energy.cxx src/evolve_pressure.cxx src/evolve_momentum.cxx + src/guarded_options.cxx src/isothermal.cxx src/quasineutral.cxx src/diamagnetic_drift.cxx @@ -93,6 +94,7 @@ set(HERMES_SOURCES src/noflow_boundary.cxx src/neutral_parallel_diffusion.cxx src/neutral_boundary.cxx + src/permissions.cxx src/polarisation_drift.cxx src/solkit_neutral_parallel_diffusion.cxx src/hydrogen_charge_exchange.cxx @@ -113,6 +115,7 @@ set(HERMES_SOURCES include/anomalous_diffusion.hxx include/classical_diffusion.hxx include/binormal_stpm.hxx + include/braginskii_closure.hxx include/braginskii_collisions.hxx include/braginskii_conduction.hxx include/braginskii_electron_viscosity.hxx @@ -133,6 +136,7 @@ set(HERMES_SOURCES include/fixed_density.hxx include/fixed_fraction_ions.hxx include/fixed_velocity.hxx + include/guarded_options.hxx include/neutral_full_velocity.hxx include/hermes_utils.hxx include/hydrogen_charge_exchange.hxx @@ -144,6 +148,7 @@ set(HERMES_SOURCES include/neutral_parallel_diffusion.hxx include/solkit_neutral_parallel_diffusion.hxx include/noflow_boundary.hxx + include/permissions.hxx include/polarisation_drift.hxx include/quasineutral.hxx include/reaction.hxx @@ -279,6 +284,7 @@ if(HERMES_TESTS) hermes_add_integrated_test(alfven-wave) hermes_add_integrated_test(collfreq-braginskii-afn) hermes_add_integrated_test(collfreq-multispecies) + hermes_add_integrated_test(component-order) # Unit tests option(HERMES_UNIT_TESTS "Build the unit tests" ON) diff --git a/docs/sphinx/boundary_conditions.rst b/docs/sphinx/boundary_conditions.rst index 9cfc2ef33..8a5c4798f 100644 --- a/docs/sphinx/boundary_conditions.rst +++ b/docs/sphinx/boundary_conditions.rst @@ -250,7 +250,7 @@ The boundary fluxes might be set by sheath boundary conditions, which potentially depend on the density and temperature of all species. Recycling therefore can't be calculated until all species boundary conditions have been set. It is therefore expected that this component is a top-level -component (i.e. in the `Hermes` section) which comes after boundary conditions are set. +component (i.e. in the `Hermes` section). Recycling has been implemented at the target, the SOL edge and the PFR edge. Each is off by default and must be activated with a separate flag. Each can be @@ -620,4 +620,4 @@ Note that if you have the density controller enabled, it will work to counteract function = 0.01 source = Pe:source source_time_dependent = true - source_prefactor = Pe:source_prefactor \ No newline at end of file + source_prefactor = Pe:source_prefactor diff --git a/docs/sphinx/closure.rst b/docs/sphinx/closure.rst index 7ce0144be..ed90ed926 100644 --- a/docs/sphinx/closure.rst +++ b/docs/sphinx/closure.rst @@ -39,7 +39,12 @@ The parallel projection of diffusion from the wall in 1D is captured in the :ref:`neutral_parallel_diffusion` top-level component, while both parallel Braginskii transport and perpendicular pressure-diffusion for 2D/3D are captured in the :ref:`neutral_mixed` species-level component. - + +A user can automatically activate all of these components at once +using the `BraginskiiClosure` component. + +.. doxygenclass:: BraginskiiClosure + :members: Collision frequency selection @@ -262,8 +267,7 @@ Input This top-level component calculates the frictional forces between each pair of species for which collisional frequencies have been calculated -(see :ref:`Braginskii Collisions`). As such, it must be run after -`braginskii_collisions`. If the option `frictional_heating` is +(see `Braginskii Collisions component`_). If the option `frictional_heating` is enabled then it will also calculate the energy source arising from friction. @@ -346,8 +350,8 @@ Braginskii Heat Exchange Input ----- This top-level component calculates the heat exchange between species -due to collisions (see :ref:`Braginskii Collisions`). As such, it must be run after -`braginskii_collisions`. There are no configurations for this component. +due to collisions (see `Braginskii Collisions component`_). There are no +configurations for this component. Theory ------ diff --git a/docs/sphinx/developer.rst b/docs/sphinx/developer.rst index 153775e9c..9a40acccf 100644 --- a/docs/sphinx/developer.rst +++ b/docs/sphinx/developer.rst @@ -181,7 +181,9 @@ are of unit length. See relevant `BOUT++ docs `_ for more info. There is also a data type called ``Options`` which is equivalent to a Python dictionary with extra functionality, and is used to store input -options, the entire simulation state and many other data. +options, the entire simulation state and many other data. Finally, +there is the ``GuardedOptions`` datatype, which wraps an ``Options`` +object and controls access to its contents. Adding new settings @@ -265,7 +267,7 @@ the variables in one place, which could allow some components to overwrite other In ``component.hxx`` there is the function ``get``, which once called sets the "final" and "final-domain" attributes: -.. code-bloc:: ini +.. code-block:: ini T get(const Options& option, const std::string& location = "") { #if CHECKLEVEL >= 1 @@ -328,6 +330,9 @@ And there is a corresponding ``setBoundary`` that can be used for BC operations: return option; } +All of these functions are overloaded to accept both `Options` and +`GuardedOptions` objects. + These functions take a second argument which tells you where they were set, which is easier for debugging. They are wrapped into additional functions, ``GET_VALUE`` and ``GET_NOBOUNDARY`` which automatically include this argument. @@ -516,21 +521,28 @@ Notes: - The species name convention is that the charge state is last, after the `+` or `-` sign: `n2+` is a singly charged nitrogen molecule, while `n+2` is a +2 charged nitrogen atom. + Components ~~~~~~~~~~~~~~ The basic building block of all Hermes-3 models is the `Component`. This defines an interface to a class which takes a state -(a tree of dictionaries/maps), and transforms (modifies) it. After -all components have modified the state in turn, all components may -then implement a `finally` method to take the final state but not +(a tree of dictionaries/maps) and transforms (modifies) it. This is +done by calling the public `Component::transform` method. This will +call the private `Component::transform_impl` method, which must be +overriden for each Component implementation. + +After all components have modified the state in turn, all components +may then implement a `finally` method to take the final state but not modify it. This allows two components to depend on each other, but makes debugging and testing easier by limiting the places where the state can be modified. .. doxygenstruct:: Component :members: + :protected-members: + :private-members: Components are usually defined in separate files; sometimes multiple components in one file if they are small and related to each other (e.g. @@ -552,7 +564,7 @@ file using a code like:: where `MyComponent` is the component class, and "mycomponent" is the name that can be used in the BOUT.inp settings file to create a component of this type. Note that the name can be any string except it -can't contain commas or brackets (), and shouldn't start or end with +can't contain commas or brackets, and shouldn't start or end with whitespace. Inputs to the component constructors are: @@ -566,11 +578,61 @@ The `name` is a string labelling the instance. The `alloptions` tree contains at * `alloptions[name]` options for this instance * `alloptions['units']` +All component constructors must pass a `Permissions` object to the +constructor on the `Component::Component` base class. This specifies +which variables will be read/written by the `Component::transform` +method and will be used to construct a `GuardedOptions` object to be +passed into `Component::transform_impl`. The `Permissions` object +(`Component::state_variable_access`) can be further updated in the +body of the constructor of your component, based on what +configurations were specified in ``alloptions``. You should give read +and write permissions to the minimum number of variables necessary, to +avoid circular dependencies arising among components. + + +Component Permissions +````````````````````` + +All component constructors must pass a `Permissions` object (see +below) to the constructor on the `Component::Component` base +class. This specifies which variables will be read/written by the +`Component::transform` method and will be used to construct a +`GuardedOptions` object to be passed into +`Component::transform_impl`. The `Permissions` object can be further +updated in the body of the constructor of your component using the +`Component::setPermissions` and `Component::substitutePermissions` +methods. You should give read and write permissions to the minimum +number of variables necessary, to avoid circular dependencies arising +among components. + +A number of substitutions will automatically be performed on your +permissions (see `Permission Substitution`_), so that you can specify +permissions for some variables for each species. For example, the +following permissions would give read access to pressure for all +species and density of ions:: + + MyComponent::MyComponent(const std::string &name, Options &options, + Solver *solver) : Component({readOnly("species:{all_species}:pressure"), + readOnly("species:{ions}:density")}) {} + +See the documentation for `Component::declareAllSpecies` for a list of +all substitutions that will be performed. + +Components may request the creation of additional components, upon +which they depend. This is done by overriding the +`Component::additionalComponents` method, which returns a list of +`ComponentInformation` structs that specify the names and types of +components required. These extra components will use the default +settings for components of this type, unless other values are +specified in a section of the input file with the component name. + +.. doxygenstruct:: ComponentInformation + :members: + Component scheduler ~~~~~~~~~~~~~~ - The simulation model is created in `Hermes::init` by a call to the `ComponentScheduler`:: scheduler = ComponentScheduler::create(options, Options::root(), solver); @@ -580,9 +642,17 @@ and then in `Hermes::rhs` the components are run by a call:: scheduler->transform(state); The call to `ComponentScheduler::create` treats the "components" -option as a comma-separated list of names. The order of the components -is the order that they are run in. For each name in the list, the -scheduler looks up the options under the section of that name. +option as a comma-separated list of names. For each name in the list, +the scheduler looks up the options under the section of that name. If +any of the listed components request further components +(via the `Component::AdditionalComponents` method) then these will be +created too. The +``ComponentScheduler`` will use permission information stored by each +component in `Component::state_variable_access` to work out the order +to execute components. It will ensure that all writes to a variable +have occurred before the first time it is read. If there is a variable +needed by some component which is never set or if there is a circular +dependency then it will throw an exception. .. code-block:: ini @@ -599,8 +669,9 @@ scheduler looks up the options under the section of that name. This would create two `Component` objects, of type `component1` and `component2`. Each time `Hermes::rhs` is run, the `transform` -functions of `component1` amd then `component2` will be called, -followed by their `finally` functions. +functions of `component1` and `component2` will be called, with the +order depending on what state variables each reads and writes. This is +followed by a call to their `finally` functions. It is often useful to group components together, for example to define the governing equations for different species. A `type` setting @@ -622,13 +693,211 @@ of components # options to control component3 This will create three components, which will be run in the order -`component1`, `component2`, `component3`: First all the components -in `group1`, and then `component3`. +`component1`, `component2`, `component3`: First all the components in +`group1`, and then `component3`. Grouped components will be sorted +individually when determining the run order; they may not be run +together. .. doxygenclass:: ComponentScheduler :members: +Permissions +~~~~~~~~~~~~~~ + +The ``Permissions`` class can be used to store information about which +variables within an ``Options`` object are allowed to be accessed and +for what purpose. This is used to control the variables used by a +``Component``. There is a hierarchy of four types of increasing +permission. These are expressed using the +`PermissionTypes` `enum `__: + +#. **ReadIfSet:** Only allowed to read variable if it is already set. +#. **Read:** Can read the contents of the variable. Assumes it has already been set. +#. **Write:** Can write variable. Makes no assumption about whether it has already been written or will be written again in future. +#. **Final:** This will be the last component to write to the variable. Only one component may have ``Final`` permission for a given variable. + +The order these per permissions are listed in is significant: each +higher permission implies a component also has all lower permissions. E.g., +write permission implies read permission as well. + +Declaring Permissions for Particular Variables +`````````````````````````````````````````````` + +Permission information for a variable is stored in a +`Permissions::VarRights` object. The overwhelming majority of the +permissions you would want to create can be constructed using one of +the provided convenience-functions. For example:: + + Permissions::VarRights read_e_pressure = readOnly("species:e:pressure"); + Permissions::VarRights write_d_density = readWrite("species:d:density"); + Permissions::VarRights read_e_velocity_in_interior_if_set = + readIfSet("species:e:velocity", Regions::Interior); + +Permissions can be set to apply only to a particular region of the +domain (e.g., the boundary or the interior) using a `Regions` enum +(see `Specifying a Region`_). + +Creating Permissions Objects +```````````````````````````` + +Permission data like that created in the previous example can be used +to construct a ``Permissions`` object. These objects describe the +permissions for multiple variables.:: + + Permissions p({readOnly("time"), + readOnly("species:e:pressure"), + readWrite("species:e:momentum", Regions::Interior)}); + +A permission applied to a section of an ``Options`` object will apply +to all variables contained within that section, unless a more specific +permission is also set. Therefore, if we have a state with variables +``species:e:pressure``, ``species:e:density``, ``species:e:velocity``, +and ``species:e:momentum``, then the following are equivalent:: + + Permissions p({readOnly("species:e"), + readWrite("species:e:momentum")}); + Permissions p({readOnly("species:e:pressure"), + readOnly("species:e:density"), + readOnly("species:e:velocity"), + readWrite("species:e:momentum")}); + +Specifying a Region +``````````````````` + +The `PermissionTypes` are applied to particular regions of the domain. +This allows, e.g., for there to be read permissions for the interior +of the domain but write permissions for the boundaries. Regions are +expressed using the `Permissions::Regions` enum, which functions as a `bitset +`__. You can combine regions +using bitwise logical operators. + +.. doxygengroup:: RegionsGroup + :members: + +Permission Substitution +``````````````````````` + +Variable names can include labels, marked in curly-braces, that will +later be substituted (using `Permissions::substitute` and +`Component::substitutePermissions`). Substitutions are necessary +because, when declaring permissions for a `Component`, you may need to +express that it can access some variable for all species (or all ions, +all neutrals, etc.), but you won't yet know the names of all the +species. For example, if you need to read the density of all species +and write the collision frequency of all ions then you would write:: + + Permissions p({readOnly("species:{all_spcies}:density"), + readWrite("species:{ions}:collision_frequency"}); + +If there are species e, d, d+, h, and h+ then the above will be +equivalent to:: + + Permissions p({readOnly("species:e:density"), + readOnly("species:d:density"), + readOnly("species:d+:density"), + readOnly("species:h:density"), + readOnly("species:h+:density"), + readWrite("species:d+:collision_frequency"), + readWrite("species:h+:collision_frequency")}); + +These substitutions will be performed in +`Component::declareAllSpecies`. See the documentation for that method +for a full list of the substitutions which it can perform. + +It can also be useful to define your own substitutions, to save +repetitive declarations. For example, you could declare read +permissions for electron density, pressure, temperature, velocity, and +momentum as follows:: + + Permissions p({readOnly("species:e:{inputs}"); + p.substitute({"density", "pressure", "temperature", "velocity", "momentum"}); + +This is equivalent to having written:: + + Permissions p({readOnly("species:e:density"), + readOnly("species:e:pressure")}, + readOnly("species:e:temperature")}, + readOnly("species:e:velocity")}, + readOnly("species:e:momentum")}); + +Permission Factory Functions +```````````````````````````` + +.. doxygengroup:: PermissionFactories + :members: + +Permissions Class +````````````````` +.. doxygenclass:: Permissions + :members: + +Further Implementation Details +`````````````````````````````` + +The above information should be sufficient for users that are +developing or modifying components. The following explains in more +detail how permission data is stored and should be read by anyone +looking to modify the `Permissions` or `GuardedOptions` classes. + +Permission information for a variable gets stored in +`Permissions::AccessRights` objects, which are arrays of +`Regions`. Each element of the array corresponds to information about +a permission level: ``{read_if_set, read, write, final}``. To access +the element for a desired permission level, you can index the array +with the corresponding member of the `PermissionTypes` enum:: + + Permissions::AccessRights rights; + Regions read_regions = rights[PermissionTypes::Read]; + Regions write_regions = rights[PermissionTypes::Write]; + +The contents of each element of an `Permissions::AccessRights` array +is the set of regions for which the permissions apply. For example:: + + Permissions::AccessRights read_boundaries_if_set = + {Regions::Boundaries, Regions::Nowhere, Regions::Nowhere, + Regions::Nowhere}; + Permissions::AccessRights read_interior_write_boundaries = + {Regions::Nowhere, Regions::Interior, Regions::Boundaries, + Regions::Nowhere}; + Permissions::AccessRights final_write_all_regions = + {Regions::Nowhere, Regions::Nowhere, Regions::Nowhere, Regions::All}; + +The `Permissions::VarRights` struct is used to pair a variable name +with a `Permissions::AccessRights` array containing the permission +information for that variable. + + +GuardedOptions +~~~~~~~~~~~~~~ + +``GuardedOptions`` objects combine a `Permissions` object and an +`Options` object. They can be indexed just like normal ``Options`` +objects but will return another ``GuardedOptions``, wrapping the +result. In order to read or write the contents of a ``GuardedOptions`` +object you must use the ``get()`` or ``getWritable()`` methods, +respectively. These will return the underlying (const) ``Options`` +object, if you have the necessary permissions to access it. Otherwise, +they will raise an exception. + +If ``CHECKLEVEL`` is 1 or above, then the ``GuardedOptions`` will track +which variables have actually been accessed. Lists of +unread/unwritten variables can be returned with the ``unreadItems()`` +and ``unwrittenItems()`` methods. If ``CHECKLEVEL`` is zero then +calling these methods will raise an exception. + +.. doxygenclass:: GuardedOptions + :members: + +.. note:: + When indexing a ``GuardedOptions`` object, it will create a new + ``GuardedOptions`` on-demand. This is unlike with a normal + ``Options`` object which returns a reference to a preexisting child + ``Options`` object. You generally should not store + ``GuardedOptions`` by reference. You may be able to pass them by + reference, but this requires you to think carefully about whether + the argument is going to be an r-value or an l-value. + .. _sec-tests: Tests @@ -893,4 +1162,4 @@ There are two simple integrated tests to make sure that the collision frequency across `neutral_mixed`, `evolve_pressure`, `ion_viscosity` and `neutral_parallel_diffusion`. A minimal 3D geometry is run for one RHS evaluation, and the test checks the log file to make sure the correct collisionalities were selected. One of the tests is for the `multispecies` -mode across all components, while the other is for `braginskii` for plasma and `afn` for neutrals. \ No newline at end of file +mode across all components, while the other is for `braginskii` for plasma and `afn` for neutrals. diff --git a/docs/sphinx/equations.rst b/docs/sphinx/equations.rst index f04795ea2..eb59424de 100644 --- a/docs/sphinx/equations.rst +++ b/docs/sphinx/equations.rst @@ -120,9 +120,8 @@ quasineutral ~~~~~~~~~~~~ This component sets the density of one species, so that the overall -charge density is zero everywhere. This must therefore be done after -all other charged species densities have been calculated. It only -makes sense to use this component for species with a non-zero charge. +charge density is zero everywhere. It only makes sense to use this +component for species with a non-zero charge. .. doxygenstruct:: Quasineutral :members: @@ -229,8 +228,8 @@ energy, :math:`\mathcal{E}`: \mathcal{E} = \frac{1}{\gamma - 1} P + \frac{1}{2}m nv_{||}^2 Note that this component requires the parallel velocity :math:`v_{||}` -to calculate the pressure. It must therefore be listed after a component -that sets the velocity, such as `evolve_momentum`: +to calculate the pressure. It must therefore be listed alongside a +component that sets the velocity, such as `evolve_momentum`: .. code-block:: ini @@ -294,13 +293,6 @@ conduction for all species that use :ref:`evolve_pressure` or desired for a particular species then it can be turned off by setting `thermal_conduction = false` in the input options for that species. -This component requires a collision time to have been calculated -(i.e., with the :ref:`Braginskii Collisions` component). It is -recommended that this be one of the last component to run, to ensure density, -pressure, and temperature have their final values. However, it must be -run before :ref:`Recycling`, as that component will need to use the -`energy_flow_ylow` value, to which conduction contributes. - The choice of collision frequency used for conduction is set by the flag `conduction_collisions_mode`: `multispecies` uses all available collision frequencies involving the chosen species, while `braginskii` @@ -382,7 +374,7 @@ using flows already calculated for other species. It is used like `quasineutral` .. code-block:: ini [hermes] - components = h+, ..., e, ... # Note: e after all other species + components = h+, ..., e, ... [e] type = ..., zero_current,... # Set e:velocity @@ -436,7 +428,7 @@ The electron parallel viscosity is \eta_e = \frac{4}{3} 0.73 p_e \tau_e where :math:`\tau_e` is the electron collision time. The collisions between electrons -and all other species therefore need to be calculated before this component is run: +and all other species therefore needs to be calculated: .. code-block:: ini @@ -451,9 +443,8 @@ and all other species therefore need to be calculated before this component is r braginskii_ion_viscosity ~~~~~~~~~~~~~~~~~~~~~~~~ -Adds ion viscosity terms to all charged species that are not electrons. -The collision frequency is required so this is a top-level component that -must be calculated after collisions: +Adds ion viscosity terms to all charged species that are not +electrons, calculated using collision frequencies. .. code-block:: ini @@ -623,8 +614,7 @@ has cross-field transport. This discrepancy is due to historical reasons and wil 1D: neutral_parallel_diffusion ~~~~~~~~~~~~~~~~~~~~~~~~~~ -This adds diffusion to **all** neutral species (those with no or zero charge), -because it needs to be calculated after the collision frequencies are known. +This adds diffusion to **all** neutral species (those with no or zero charge). .. code-block:: ini @@ -982,9 +972,8 @@ electrostatic potential :math:`\phi` in the frame of the fluid, with an ion diamagnetic contribution. This is calculated by inverting a Laplacian equation similar to that solved in the vorticity equation. -This component needs to be run after all other currents have been -calculated. It marks currents as used, so out-of-order modifications -should raise errors. +This component will be run after all other currents have been +calculated. See the `examples/blob2d-vpol` example, which contains: diff --git a/docs/sphinx/examples.rst b/docs/sphinx/examples.rst index 92345d870..e9d3d588f 100644 --- a/docs/sphinx/examples.rst +++ b/docs/sphinx/examples.rst @@ -276,9 +276,10 @@ so that the equation solved is where :math:`T_e` is the fixed electron temperature (5eV). -The :ref:`vorticity` component uses the pressure to calculate the diamagnetic current, -so must come after the `e` component. This component then calculates the potential. -Options to control the vorticity component are set in the `[vorticity]` section. +The :ref:`vorticity` component uses the pressure to calculate the +diamagnetic current. This component then calculates the potential. +Options to control the vorticity component are set in the +`[vorticity]` section. .. math:: @@ -287,8 +288,7 @@ Options to control the vorticity component are set in the `[vorticity]` section. \nabla\cdot\left(\frac{1}{B^2}\nabla_\perp\phi\right) = \omega \end{aligned} -The `sheath_closure` component uses the potential, so must come after :ref:`vorticity`. -Options are also set as +The `sheath_closure` component uses the potential. Options are also set as .. code-block:: ini diff --git a/docs/sphinx/inputs.rst b/docs/sphinx/inputs.rst index ff7a936b7..fbaac1d7f 100644 --- a/docs/sphinx/inputs.rst +++ b/docs/sphinx/inputs.rst @@ -19,8 +19,9 @@ number of output timesteps ``nout`` and the output timestep size Note that the solver timestep is adaptive and not user-settable. This is followed by ``[mesh]``, ``[solver]`` and ``[hermes]`` headers, where the ``[hermes]`` -section defines the list of components used. The component order -matters, as the components are executed in order. +section defines the list of components used. The component order doesn't +matters, as they will be sorted to ensure state variables are set +before they are used. .. code-block:: ini diff --git a/hermes-3.cxx b/hermes-3.cxx index 46f6a7f6e..aea8a87bf 100644 --- a/hermes-3.cxx +++ b/hermes-3.cxx @@ -30,6 +30,7 @@ #include "include/amjuel_hydrogen.hxx" #include "include/anomalous_diffusion.hxx" #include "include/binormal_stpm.hxx" +#include "include/braginskii_closure.hxx" #include "include/braginskii_collisions.hxx" #include "include/braginskii_conduction.hxx" #include "include/braginskii_electron_viscosity.hxx" diff --git a/include/adas_carbon.hxx b/include/adas_carbon.hxx index ed82c1b3f..730ac416a 100644 --- a/include/adas_carbon.hxx +++ b/include/adas_carbon.hxx @@ -37,10 +37,12 @@ constexpr std::initializer_list carbon_species_name<0>{'c'}; template struct ADASCarbonIonisation : public OpenADAS { ADASCarbonIonisation(std::string, Options& alloptions, Solver*) - : OpenADAS(alloptions["units"], "scd96_c.json", "plt96_c.json", level, + : OpenADAS(alloptions["units"], "scd96_c.json", "plt96_c.json", + carbon_species_name, carbon_species_name, level, -carbon_ionisation_energy[level]) {} - void transform(Options& state) override { +private: + void transform_impl(GuardedOptions& state) override { calculate_rates( state["species"]["e"], // Electrons state["species"][carbon_species_name], // From this ionisation state @@ -58,10 +60,12 @@ template struct ADASCarbonRecombination : public OpenADAS { /// @param alloptions The top-level options. Only uses the ["units"] subsection. ADASCarbonRecombination(std::string, Options& alloptions, Solver*) - : OpenADAS(alloptions["units"], "acd96_c.json", "prb96_c.json", level, + : OpenADAS(alloptions["units"], "acd96_c.json", "prb96_c.json", + carbon_species_name, carbon_species_name, level, carbon_ionisation_energy[level]) {} - void transform(Options& state) override { +private: + void transform_impl(GuardedOptions& state) override { calculate_rates( state["species"]["e"], // Electrons state["species"][carbon_species_name], // From this ionisation state @@ -76,10 +80,13 @@ template struct ADASCarbonCX : public OpenADASChargeExchange { /// @param alloptions The top-level options. Only uses the ["units"] subsection. ADASCarbonCX(std::string, Options& alloptions, Solver*) - : OpenADASChargeExchange(alloptions["units"], "ccd96_c.json", level) {} + : OpenADASChargeExchange(alloptions["units"], "ccd96_c.json", + carbon_species_name, {Hisotope}, + carbon_species_name, {Hisotope, '+'}, level) {} - void transform(Options& state) override { - Options& species = state["species"]; +private: + void transform_impl(GuardedOptions& state) override { + GuardedOptions species = state["species"]; calculate_rates( species["e"], // Electrons species[carbon_species_name], // From this ionisation state diff --git a/include/adas_lithium.hxx b/include/adas_lithium.hxx index 20cc6f349..476e62333 100644 --- a/include/adas_lithium.hxx +++ b/include/adas_lithium.hxx @@ -40,10 +40,12 @@ constexpr std::initializer_list lithium_species_name<0>{'l', 'i'}; template struct ADASLithiumIonisation : public OpenADAS { ADASLithiumIonisation(std::string, Options& alloptions, Solver*) - : OpenADAS(alloptions["units"], "scd96_li.json", "plt96_li.json", level, + : OpenADAS(alloptions["units"], "scd96_li.json", "plt96_li.json", + lithium_species_name, lithium_species_name, level, -lithium_ionisation_energy[level]) {} - void transform(Options& state) override { +private: + void transform_impl(GuardedOptions& state) override { calculate_rates( state["species"]["e"], // Electrons state["species"][lithium_species_name], // From this ionisation state @@ -61,10 +63,12 @@ template struct ADASLithiumRecombination : public OpenADAS { /// @param alloptions The top-level options. Only uses the ["units"] subsection. ADASLithiumRecombination(std::string, Options& alloptions, Solver*) - : OpenADAS(alloptions["units"], "acd96_li.json", "prb96_li.json", level, + : OpenADAS(alloptions["units"], "acd96_li.json", "prb96_li.json", + lithium_species_name, lithium_species_name, level, lithium_ionisation_energy[level]) {} - void transform(Options& state) override { +private: + void transform_impl(GuardedOptions& state) override { calculate_rates( state["species"]["e"], // Electrons state["species"][lithium_species_name], // From this ionisation state @@ -79,10 +83,13 @@ template struct ADASLithiumCX : public OpenADASChargeExchange { /// @param alloptions The top-level options. Only uses the ["units"] subsection. ADASLithiumCX(std::string, Options& alloptions, Solver*) - : OpenADASChargeExchange(alloptions["units"], "ccd89_li.json", level) {} + : OpenADASChargeExchange(alloptions["units"], "ccd89_li.json", + lithium_species_name, {Hisotope}, + lithium_species_name, {Hisotope, '+'}, level) {} - void transform(Options& state) override { - Options& species = state["species"]; +private: + void transform_impl(GuardedOptions& state) override { + GuardedOptions species = state["species"]; calculate_rates( species["e"], // Electrons species[lithium_species_name], // From this ionisation state diff --git a/include/adas_neon.hxx b/include/adas_neon.hxx index d055c403f..f2030868f 100644 --- a/include/adas_neon.hxx +++ b/include/adas_neon.hxx @@ -40,10 +40,12 @@ constexpr std::initializer_list neon_species_name<0>{'n', 'e'}; template struct ADASNeonIonisation : public OpenADAS { ADASNeonIonisation(std::string, Options& alloptions, Solver*) - : OpenADAS(alloptions["units"], "scd96_ne.json", "plt96_ne.json", level, + : OpenADAS(alloptions["units"], "scd96_ne.json", "plt96_ne.json", + neon_species_name, neon_species_name, level, -neon_ionisation_energy[level]) {} - void transform(Options& state) override { +private: + void transform_impl(GuardedOptions& state) override { calculate_rates( state["species"]["e"], // Electrons state["species"][neon_species_name], // From this ionisation state @@ -61,10 +63,12 @@ template struct ADASNeonRecombination : public OpenADAS { /// @param alloptions The top-level options. Only uses the ["units"] subsection. ADASNeonRecombination(std::string, Options& alloptions, Solver*) - : OpenADAS(alloptions["units"], "acd96_ne.json", "prb96_ne.json", level, + : OpenADAS(alloptions["units"], "acd96_ne.json", "prb96_ne.json", + neon_species_name, neon_species_name, level, neon_ionisation_energy[level]) {} - void transform(Options& state) override { +private: + void transform_impl(GuardedOptions& state) override { calculate_rates( state["species"]["e"], // Electrons state["species"][neon_species_name], // From this ionisation state @@ -79,10 +83,13 @@ template struct ADASNeonCX : public OpenADASChargeExchange { /// @param alloptions The top-level options. Only uses the ["units"] subsection. ADASNeonCX(std::string, Options& alloptions, Solver*) - : OpenADASChargeExchange(alloptions["units"], "ccd89_ne.json", level) {} + : OpenADASChargeExchange(alloptions["units"], "ccd89_ne.json", + neon_species_name, {Hisotope}, + neon_species_name, {Hisotope, '+'}, level) {} - void transform(Options& state) override { - Options& species = state["species"]; +private: + void transform_impl(GuardedOptions& state) override { + GuardedOptions species = state["species"]; calculate_rates( species["e"], // Electrons species[neon_species_name], // From this ionisation state diff --git a/include/adas_reaction.hxx b/include/adas_reaction.hxx index 0c584949b..3c2cd0021 100644 --- a/include/adas_reaction.hxx +++ b/include/adas_reaction.hxx @@ -53,16 +53,30 @@ struct OpenADAS : public ReactionBase { /// /// Notes /// - The rate and radiation file names have "json_database/" prepended - /// + /// OpenADAS(const Options& units, const std::string& rate_file, - const std::string& radiation_file, std::size_t level, BoutReal electron_heating) - : rate_coef(std::string("json_database/") + rate_file, level), + const std::string& radiation_file, std::string from_ion, std::string to_ion, + std::size_t level, BoutReal electron_heating) + : ReactionBase({readIfSet("species:{sp}:charge"), readOnly("species:{sp}:AA"), + readOnly("species:{from_ion}:{val}"), readOnly("species:e:{e_val}"), + readWrite("species:{sp}:{w_val}"), + readWrite("species:e:{ew_val}")}), + rate_coef(std::string("json_database/") + rate_file, level), radiation_coef(std::string("json_database/") + radiation_file, level), electron_heating(electron_heating) { // Get the units Tnorm = get(units["eV"]); Nnorm = get(units["inv_meters_cubed"]); FreqNorm = 1. / get(units["seconds"]); + substitutePermissions("val", {"density", "temperature", "velocity"}); + substitutePermissions("e_val", {"density", "temperature"}); + substitutePermissions("w_val", + {"density_source", "momentum_source", "energy_source"}); + // FIXME: electron density_source only written if from_ion charge != to_ion charge. + substitutePermissions("ew_val", + {"density_source", "momentum_source", "energy_source"}); + substitutePermissions("sp", {from_ion, to_ion}); + substitutePermissions("from_ion", {from_ion}); } /// Perform the calculation of rates, and transfer of particles/momentum/energy @@ -70,7 +84,7 @@ struct OpenADAS : public ReactionBase { /// @param electron The electron species e.g. state["species"]["e"] /// @param from_ion The ion on the left of the reaction /// @param to_ion The ion on the right of the reaction - void calculate_rates(Options& electron, Options& from_ion, Options& to_ion); + void calculate_rates(GuardedOptions && electron, GuardedOptions && from_ion, GuardedOptions && to_ion); private: OpenADASRateCoefficient rate_coef; ///< Reaction rate coefficient OpenADASRateCoefficient radiation_coef; ///< Energy loss (radiation) coefficient @@ -81,12 +95,23 @@ private: }; struct OpenADASChargeExchange : public ReactionBase { - OpenADASChargeExchange(const Options& units, const std::string& rate_file, std::size_t level) - : rate_coef(std::string("json_database/") + rate_file, level) { + OpenADASChargeExchange(const Options& units, const std::string& rate_file, + std::string from_A, std::string from_B, std::string to_A, + std::string to_B, std::size_t level) + : ReactionBase({readIfSet("species:{sp}:charge"), readOnly("species:{sp}:AA"), + readOnly("species:{from_ion}:{val}"), readOnly("species:e:{e_val}"), + readWrite("species:{sp}:{w_val}")}), + rate_coef(std::string("json_database/") + rate_file, level) { // Get the units Tnorm = get(units["eV"]); Nnorm = get(units["inv_meters_cubed"]); FreqNorm = 1. / get(units["seconds"]); + substitutePermissions("val", {"density", "temperature", "velocity"}); + substitutePermissions("e_val", {"density", "temperature"}); + substitutePermissions("w_val", + {"density_source", "momentum_source", "energy_source"}); + substitutePermissions("sp", {from_A, from_B, to_A, to_B}); + substitutePermissions("from_ion", {from_A, from_B}); } /// Perform charge exchange /// @@ -95,8 +120,8 @@ struct OpenADASChargeExchange : public ReactionBase { /// from_A and to_A must have the same atomic mass /// from_B and to_B must have the same atomic mass /// The charge of from_A + from_B must equal the charge of to_A + to_B - void calculate_rates(Options& electron, Options& from_A, Options& from_B, Options& to_A, - Options& to_B); + void calculate_rates(GuardedOptions && electron, GuardedOptions && from_A, GuardedOptions && from_B, GuardedOptions && to_A, + GuardedOptions && to_B); private: OpenADASRateCoefficient rate_coef; ///< Reaction rate coefficient diff --git a/include/amjuel_helium.hxx b/include/amjuel_helium.hxx index 95d8c88ba..954c4553f 100644 --- a/include/amjuel_helium.hxx +++ b/include/amjuel_helium.hxx @@ -11,7 +11,7 @@ */ struct AmjuelHeIonisation01 : public AmjuelReaction { AmjuelHeIonisation01(std::string name, Options& alloptions, Solver*) - : AmjuelReaction(name, "iz", "H.x_2.3.9a", "he", "he+", alloptions) { + : AmjuelReaction(name, "iz", "H.x_2.3.9a", "he + e -> he+ + 2e", "he", "he+", alloptions) { rate_multiplier = alloptions[std::string("he")]["ionisation_rate_multiplier"] .doc("Scale the ionisation rate by this factor") @@ -32,7 +32,7 @@ struct AmjuelHeIonisation01 : public AmjuelReaction { */ struct AmjuelHeRecombination10 : public AmjuelReaction { AmjuelHeRecombination10(std::string name, Options& alloptions, Solver*) - : AmjuelReaction(name, "rec", "H.x_2.3.13a", "he+", "he", alloptions) { + : AmjuelReaction(name, "rec", "H.x_2.3.13a", "he+ + e -> he", "he+", "he", alloptions) { rate_multiplier = alloptions[name]["recombination_rate_multiplier"] .doc("Scale the recombination rate by this factor") diff --git a/include/amjuel_hydrogen.hxx b/include/amjuel_hydrogen.hxx index f844bb96b..d55af52af 100644 --- a/include/amjuel_hydrogen.hxx +++ b/include/amjuel_hydrogen.hxx @@ -19,9 +19,9 @@ static std::map long_reaction_types_map = { template struct AmjuelHydIsotopeReaction : public AmjuelReaction { AmjuelHydIsotopeReaction(std::string name, std::string short_reaction_type, - std::string amjuel_label, std::string from_species, + std::string amjuel_label, const std::string & reaction_str, std::string from_species, std::string to_species, Options& alloptions) - : AmjuelReaction(name, short_reaction_type, amjuel_label, from_species, to_species, + : AmjuelReaction(name, short_reaction_type, amjuel_label, reaction_str, from_species, to_species, alloptions) { if (this->diagnose) { // Set up diagnostics @@ -82,7 +82,7 @@ struct AmjuelHydIsotopeReaction : public AmjuelReaction { template struct AmjuelHydRecombinationIsotope : public AmjuelHydIsotopeReaction { AmjuelHydRecombinationIsotope(std::string name, Options& alloptions, Solver*) - : AmjuelHydIsotopeReaction(name, "rec", "H.x_2.1.8", {Isotope, '+'}, + : AmjuelHydIsotopeReaction(name, "rec", "H.x_2.1.8", fmt::format("{}+ + e -> {}", Isotope, Isotope), {Isotope, '+'}, {Isotope}, alloptions) { this->rate_multiplier = alloptions[{Isotope}]["K_rec_multiplier"] @@ -104,7 +104,7 @@ struct AmjuelHydRecombinationIsotope : public AmjuelHydIsotopeReaction template struct AmjuelHydIonisationIsotope : public AmjuelHydIsotopeReaction { AmjuelHydIonisationIsotope(std::string name, Options& alloptions, Solver*) - : AmjuelHydIsotopeReaction(name, "iz", "H.x_2.1.5", {Isotope}, + : AmjuelHydIsotopeReaction(name, "iz", "H.x_2.1.5", fmt::format("{} + e -> {}+ + 2e", Isotope, Isotope), {Isotope}, {Isotope, '+'}, alloptions) { this->rate_multiplier = alloptions[{Isotope}]["K_iz_multiplier"] .doc("Scale the ionisation rate by this factor") diff --git a/include/amjuel_reaction.hxx b/include/amjuel_reaction.hxx index a6fdef54c..2580a5886 100644 --- a/include/amjuel_reaction.hxx +++ b/include/amjuel_reaction.hxx @@ -33,14 +33,27 @@ static inline std::filesystem::path get_json_db_dir(Options& options) { */ struct AmjuelReaction : public Reaction { AmjuelReaction(std::string name, std::string short_reaction_type, - std::string amjuel_lbl, std::string from_species, std::string to_species, + std::string amjuel_lbl, const std::string & reaction_str, std::string from_species, std::string to_species, Options& alloptions) - : Reaction(name, alloptions), amjuel_src(std::string("Amjuel ") + amjuel_lbl), + : Reaction(name, alloptions, reaction_str), amjuel_src(std::string("Amjuel ") + amjuel_lbl), short_reaction_type(short_reaction_type), from_species(from_species), to_species(to_species), amjuel_data(get_json_db_dir(alloptions), short_reaction_type, amjuel_lbl) { this->includes_sigma_v_e = amjuel_data.includes_sigma_v_e; + // Most of the access information we need is inherited from the parent Reaction class. + // The electron velocity will be read if it is set + setPermissions(readIfSet("species:e:velocity")); + // The energy source is set for electrons + setPermissions(readWrite("species:e:energy_source")); + std::string heavy_reactant = this->parser->get_species(species_filter::reactants, + species_filter::heavy)[0], + heavy_product = this->parser->get_species(species_filter::products, + species_filter::heavy)[0], + neutral = this->parser->get_species(species_filter::neutral)[0]; + setPermissions( + readWrite(fmt::format("species:{}:collision_frequencies:{}_{}_{}", neutral, + heavy_reactant, heavy_product, short_reaction_type))); } protected: @@ -56,7 +69,7 @@ protected: virtual BoutReal eval_sigma_v_E(BoutReal T, BoutReal n) override final; virtual BoutReal eval_sigma_v(BoutReal T, BoutReal n) override final; - virtual void transform_additional(Options& state, + virtual void transform_additional(GuardedOptions& state, Field3D& reaction_rate) override final; private: diff --git a/include/anomalous_diffusion.hxx b/include/anomalous_diffusion.hxx index e8c833592..56a777013 100644 --- a/include/anomalous_diffusion.hxx +++ b/include/anomalous_diffusion.hxx @@ -24,9 +24,23 @@ struct AnomalousDiffusion : public Component { // Default false. AnomalousDiffusion(std::string name, Options &alloptions, Solver *); + void outputVars(Options &state) override; + +private: + std::string name; ///< Species name + + bool diagnose; ///< Outputting diagnostics? + bool include_D, include_chi, include_nu; ///< Which terms should be included? + Field2D anomalous_D; ///< Anomalous density diffusion coefficient + Field2D anomalous_chi; ///< Anomalous thermal diffusion coefficient + Field2D anomalous_nu; ///< Anomalous momentum diffusion coefficient + + bool anomalous_sheath_flux; ///< Allow anomalous diffusion into sheath? + /// Inputs /// - species /// - + /// - AA /// - density /// - temperature (optional) /// - velocity (optional) @@ -39,19 +53,7 @@ struct AnomalousDiffusion : public Component { /// - momentum_source /// - energy_source /// - void transform(Options &state) override; - void outputVars(Options &state) override; - -private: - std::string name; ///< Species name - - bool diagnose; ///< Outputting diagnostics? - bool include_D, include_chi, include_nu; ///< Which terms should be included? - Field2D anomalous_D; ///< Anomalous density diffusion coefficient - Field2D anomalous_chi; ///< Anomalous thermal diffusion coefficient - Field2D anomalous_nu; ///< Anomalous momentum diffusion coefficient - - bool anomalous_sheath_flux; ///< Allow anomalous diffusion into sheath? + void transform_impl(GuardedOptions& state) override; }; diff --git a/include/binormal_stpm.hxx b/include/binormal_stpm.hxx index 8652c4fdf..c077303bc 100644 --- a/include/binormal_stpm.hxx +++ b/include/binormal_stpm.hxx @@ -23,14 +23,6 @@ struct BinormalSTPM : public Component { /// BinormalSTPM(std::string name, Options& options, Solver* solver); - /// Sets - /// - species - /// - - /// - pressure correction - /// - momentum correction - /// - density correction - /// - void transform(Options& state) override; void outputVars(Options &state) override; @@ -41,6 +33,14 @@ private: Field3D nu_Theta, chi_Theta, D_Theta; ///< nu/Theta, chi/Theta, D/Theta, precalculated Field3D Theta_inv; ///< Precalculate 1/Theta + /// Sets + /// - species + /// - + /// - pressure correction + /// - momentum correction + /// - density correction + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/braginskii_closure.hxx b/include/braginskii_closure.hxx new file mode 100644 index 000000000..997b51acf --- /dev/null +++ b/include/braginskii_closure.hxx @@ -0,0 +1,68 @@ +#pragma once +#ifndef BRAGINSKII_H +#define BRAGINSKII_H + +#include + +#include "component.hxx" + +/// Meta-component to set up all components necessary for the +/// Braginskii closure: `braginskii_collisions`, +/// `braginskii_friction`, `braginskii_heat_exchange`, +/// `braginskii_conduction`, `braginskii_electron_viscosity`, +/// `braginskii_ion_viscosity`, and `braginskii_thermal_force`. Each +/// of these components will have the same name as its type. +class BraginskiiClosure : public Component { +public: + /// @param alloptions Settings, which may include + /// - + /// - electron_viscosity : bool Include electron viscosity? (default: true) + /// - ion_viscosity : bool Include ion viscosity? (default: true) + /// - thermal_force : bool Inlucde thermal force between species? (default: true) + BraginskiiClosure(std::string name, Options& alloptions, Solver*) : Component({}) { + Options& options = alloptions[name]; + electron_viscosity = options["electron_viscosity"] + .doc("Include electron viscosity terms?") + .withDefault(true); + ion_viscosity = options["ion_viscosity"] + .doc("Include ion viscosity terms?") + .withDefault(true); + thermal_force = options["thermal_force"] + .doc("Include thermal force terms?") + .withDefault(true); + } + + virtual std::vector additionalComponents() override { + std::vector result = { + {"braginskii_collisions", "braginskii_collisions"}, + {"braginskii_friction", "braginskii_friction"}, + {"braginskii_heat_exchange", "braginskii_heat_exchange"}, + {"braginskii_conduction", "braginskii_conduction"}}; + if (electron_viscosity) { + result.emplace_back("braginskii_electron_viscosity", + "braginskii_electron_viscosity"); + } + if (ion_viscosity) { + result.emplace_back("braginskii_ion_viscosity", "braginskii_ion_viscosity"); + } + if (thermal_force) { + result.emplace_back("braginskii_thermal_force", "braginskii_thermal_force"); + } + return result; + } + +private: + bool electron_viscosity; /// Whether to include electron viscosity terms + bool ion_viscosity; /// Whether to include ion viscosity terms + bool thermal_force; /// Whether to include thermal force terms + + /// Empty transform; all the work actually happens in the subcomponents. + void transform_impl(GuardedOptions&) override {} +}; + +namespace { +RegisterComponent + registercomponentbraginskiiclosure("braginskii_closure"); +} + +#endif // BRAGINSKII_H diff --git a/include/braginskii_collisions.hxx b/include/braginskii_collisions.hxx index 84a34a815..5303d72c6 100644 --- a/include/braginskii_collisions.hxx +++ b/include/braginskii_collisions.hxx @@ -43,8 +43,6 @@ struct BraginskiiCollisions : public Component { /// BraginskiiCollisions(const std::string& name, Options& alloptions, Solver*); - void transform(Options& state) override; - /// Add extra fields for output, or set attributes e.g docstrings void outputVars(Options& state) override; @@ -67,9 +65,11 @@ private: /// Save more diagnostics? bool diagnose; - /// Update collision frequencies + void transform_impl(GuardedOptions& state) override; + + /// Update collision frequencies, momentum and energy exchange /// nu_12 normalised frequency - void collide(Options& species1, Options& species2, const Field3D& nu_12); + void collide(GuardedOptions& species1, GuardedOptions& species2, const Field3D& nu_12); }; namespace { diff --git a/include/braginskii_conduction.hxx b/include/braginskii_conduction.hxx index 9a4e859c1..225cedb2d 100644 --- a/include/braginskii_conduction.hxx +++ b/include/braginskii_conduction.hxx @@ -37,26 +37,6 @@ struct BraginskiiConduction : public Component { /// BraginskiiConduction(const std::string& name, Options& alloptions, Solver*); - /// Calculate conduction of energy for each species where this has been turned on. - /// - /// Uses - /// - species - /// - - /// - AA - /// - collision_frequencies - /// - density - /// - temperature - /// - pressure - /// - /// Modifies - /// - species - /// - - /// - energy_source Conduction contribution to energy evolution - /// - kappa_par The parallel heat conduction coefficient - /// - energy_flow_ylow Energy flow diagnostics. - /// - void transform(Options& state) override; - /// Add extra fields for output, or set attributes e.g docstrings void outputVars(Options& state) override; @@ -76,6 +56,26 @@ private: all_flow_ylow_conduction; ///< Conduction energy flow diagnostics /// Save more diagnostics? std::map all_diagnose; + + /// Calculate conduction of energy for each species where this has been turned on. + /// + /// Uses + /// - species + /// - + /// - AA + /// - collision_frequencies + /// - density + /// - temperature + /// - pressure + /// + /// Modifies + /// - species + /// - + /// - energy_source Conduction contribution to energy evolution + /// - kappa_par The parallel heat conduction coefficient + /// - energy_flow_ylow Energy flow diagnostics. + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/braginskii_electron_viscosity.hxx b/include/braginskii_electron_viscosity.hxx index 68e09169e..ccb57feaa 100644 --- a/include/braginskii_electron_viscosity.hxx +++ b/include/braginskii_electron_viscosity.hxx @@ -30,6 +30,13 @@ struct BraginskiiElectronViscosity : public Component { /// Flux limiter coefficient. < 0 means no limiter BraginskiiElectronViscosity(const std::string& name, Options& alloptions, Solver*); + void outputVars(Options &state) override; + +private: + BoutReal eta_limit_alpha; ///< Flux limit coefficient + bool diagnose; ///< Output viscosity diagnostic? + Field3D viscosity; ///< The viscosity momentum source + /// Inputs /// - species /// - e @@ -42,14 +49,7 @@ struct BraginskiiElectronViscosity : public Component { /// - e /// - momentum_source /// - void transform(Options& state) override; - - void outputVars(Options& state) override; - -private: - BoutReal eta_limit_alpha; ///< Flux limit coefficient - bool diagnose; ///< Output viscosity diagnostic? - Field3D viscosity; ///< The viscosity momentum source + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/braginskii_friction.hxx b/include/braginskii_friction.hxx index e34cb2bc7..274877e72 100644 --- a/include/braginskii_friction.hxx +++ b/include/braginskii_friction.hxx @@ -19,6 +19,20 @@ struct BraginskiiFriction : public Component { /// BraginskiiFriction(const std::string& name, Options& alloptions, Solver*); + /// Add extra fields for output, or set attributes e.g docstrings + void outputVars(Options& state) override; + +private: + /// Include frictional heating term? + bool frictional_heating; + + /// Calculated friction heating and momentum rates saved for post-processing and use by + /// other components Saved in options, the BOUT++ dictionary-like object + Options friction_energy_channels, momentum_channels; + + /// Save more diagnostics? + bool diagnose; + /// Calculate transfer of momentum and energy between species due to /// friction arising from collisions. /// @@ -37,21 +51,7 @@ struct BraginskiiFriction : public Component { /// - momentum_source if species1 or species2 velocity is set /// - energy_source if velocity is set and frictional_heating /// - void transform(Options& state) override; - - /// Add extra fields for output, or set attributes e.g docstrings - void outputVars(Options& state) override; - -private: - /// Include frictional heating term? - bool frictional_heating; - - /// Calculated friction heating and momentum rates saved for post-processing and use by - /// other components Saved in options, the BOUT++ dictionary-like object - Options friction_energy_channels, momentum_channels; - - /// Save more diagnostics? - bool diagnose; + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/braginskii_heat_exchange.hxx b/include/braginskii_heat_exchange.hxx index 3135dd76f..5bea5f781 100644 --- a/include/braginskii_heat_exchange.hxx +++ b/include/braginskii_heat_exchange.hxx @@ -16,35 +16,35 @@ struct BraginskiiHeatExchange : public Component { /// BraginskiiHeatExchange(const std::string& name, Options& alloptions, Solver*); + /// Add extra fields for output, or set attributes e.g docstrings + void outputVars(Options& state) override; + +private: + /// Calculated energy transfer for post-processing and use by other components + /// Saved in options, the BOUT++ dictionary-like object + Options energy_channels; + + /// Save more diagnostics? + bool diagnose; + /// Calculate thermal energy exchange between species due to collisions. /// /// Uses /// - species /// - /// - AA - /// - charge - /// - collision_frequencies + /// - charge (if set) + /// - collision_frequencies (if section) /// - density - /// - velocity + /// - temperature (if set) /// /// Modifies /// - species /// - - /// - momentum_source if species1 or species2 velocity is set - /// - energy_source if velocity is set and frictional_heating + /// - momentum_source if species1 or species2 temperature is set + /// - energy_source if temperature is set /// - void transform(Options& state) override; - - /// Add extra fields for output, or set attributes e.g docstrings - void outputVars(Options& state) override; - -private: - /// Calculated energy transfer for post-processing and use by other components - /// Saved in options, the BOUT++ dictionary-like object - Options energy_channels; - - /// Save more diagnostics? - bool diagnose; + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/braginskii_ion_viscosity.hxx b/include/braginskii_ion_viscosity.hxx index 6cd7bcc22..9f51b1845 100644 --- a/include/braginskii_ion_viscosity.hxx +++ b/include/braginskii_ion_viscosity.hxx @@ -42,20 +42,6 @@ struct BraginskiiIonViscosity : public Component { /// BraginskiiIonViscosity(const std::string& name, Options& alloptions, Solver*); - /// Inputs - /// - species - /// - (skips "e") - /// - pressure (skips if not present) - /// - velocity (skips if not present) - /// - collision_frequency - /// - /// Sets in the state - /// - species - /// - - /// - momentum_source - /// - void transform(Options& state) override; - /// Save variables to the output void outputVars(Options& state) override; @@ -86,6 +72,26 @@ private: /// Store diagnostics for each species std::map diagnostics; + + /// Inputs + /// - species + /// - (skips "e") + /// - charge + /// - pressure (skips if not present) + /// - velocity (skips if not present) + /// - collision_frequency + /// - fields + /// - phi + /// + /// Sets in the state + /// - species + /// - + /// - momentum_source + /// - energy_source + /// - fields + /// - DivJextra + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/braginskii_thermal_force.hxx b/include/braginskii_thermal_force.hxx index be63056a0..cf8b1068f 100644 --- a/include/braginskii_thermal_force.hxx +++ b/include/braginskii_thermal_force.hxx @@ -27,7 +27,8 @@ /// - ion_ion : bool Include ion-ion elastic collisions? /// struct BraginskiiThermalForce : public Component { - BraginskiiThermalForce(const std::string& name, Options& alloptions, Solver*) { + BraginskiiThermalForce(const std::string& name, Options& alloptions, Solver*) + : Component(Permissions()) { Options& options = alloptions[name]; this->electron_ion = options["electron_ion"] .doc("Include electron-ion collisions?") @@ -42,8 +43,38 @@ struct BraginskiiThermalForce : public Component { .doc("Override the default mass restrictions for ion-ion thermal " "force calculations?") .withDefault(false); + + if (electron_ion or ion_ion) { + setPermissions(readIfSet("species:{all_species}:charge")); + } + if (electron_ion) { + // FIXME: These are only accessed if electrons present + setPermissions(readOnly("species:e:temperature")); + setPermissions(readOnly("species:{ions}:density", Regions::Interior)); + setPermissions(readWrite("species:{ions}:momentum_source")); + setPermissions(readWrite("species:e:momentum_source")); + } else if (ion_ion) { + } + if (ion_ion) { + setPermissions(readOnly("species:{ions}:AA")); + // FIXME: This is only accessed for light ions + setPermissions(readOnly("species:{ions}:temperature")); + if (!electron_ion) { + // FIXME: This is only accessed for heavy ions + setPermissions(readOnly("species:{ions}:density", Regions::Interior)); + // FIXME: This is only set for heavy and light ions, not intermediate + setPermissions(readWrite("species:{ions}:momentum_source")); + } + } } +private: + bool electron_ion; ///< Include electron-ion collisions? + bool ion_ion; ///< Include ion-ion elastic collisions? + bool override_ion_mass_restrictions; ///< Ignore default mass restrictions when + ///< calculating thermal force between ions? + bool first_time{true}; ///< True the first time transform() is called + /// Inputs /// - species /// - e [ if electron_ion true ] @@ -53,6 +84,7 @@ struct BraginskiiThermalForce : public Component { /// - /// - charge [ Checks, skips species if not set ] /// - AA + /// - density /// - temperature [ If AA < 4 i.e. "light" species ] /// /// Outputs @@ -62,15 +94,7 @@ struct BraginskiiThermalForce : public Component { /// - [ if AA < 4 ("light") or AA > 10 ("heavy") ] /// - momentum_source /// - void transform(Options& state) override; - -private: - bool electron_ion; ///< Include electron-ion collisions? - bool ion_ion; ///< Include ion-ion elastic collisions? - bool override_ion_mass_restrictions; ///< Ignore default mass restrictions when - ///< calculating thermal force between ions? - - bool first_time{true}; ///< True the first time transform() is called + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/classical_diffusion.hxx b/include/classical_diffusion.hxx index dfa71eb70..cb738cc90 100644 --- a/include/classical_diffusion.hxx +++ b/include/classical_diffusion.hxx @@ -7,8 +7,6 @@ struct ClassicalDiffusion : public Component { ClassicalDiffusion(std::string name, Options& alloptions, Solver*); - void transform(Options &state) override; - void outputVars(Options &state) override; private: Field2D Bsq; // Magnetic field squared @@ -16,6 +14,8 @@ private: bool diagnose; ///< Output additional diagnostics? Field3D Dn; ///< Particle diffusion coefficient BoutReal custom_D; ///< User-set particle diffusion coefficient override + + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/component.hxx b/include/component.hxx index 3650e0afd..07c6d051b 100644 --- a/include/component.hxx +++ b/include/component.hxx @@ -1,52 +1,215 @@ #pragma once - #ifndef HERMES_COMPONENT_H #define HERMES_COMPONENT_H -#include -#include - +#include +#include #include -#include #include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "guarded_options.hxx" +#include "permissions.hxx" +#include "hermes_utils.hxx" class Solver; // Time integrator +/// Simple struct to store information on the different types of +/// species present in a simulation +struct SpeciesInformation { + SpeciesInformation(const std::vector& electrons, + const std::vector& neutrals, + const std::vector& positive_ions, + const std::vector& negative_ions) + : electrons(std::move(electrons)), neutrals(std::move(neutrals)), + positive_ions(std::move(positive_ions)), negative_ions(std::move(negative_ions)), + ions(std::move(positive_ions)) { + finish_construction(); + } + + SpeciesInformation(const std::initializer_list species) { + for (auto& sp : species) { + // FIXME: identifySpecies only identifies positive ions + // FIXME: identifySpecies has no concept of ebeam + const SpeciesType type = identifySpeciesType(sp); + if (type == SpeciesType::electron) { + electrons.push_back(sp); + } else if (type == SpeciesType::ion) { + positive_ions.push_back(sp); + } else if (type == SpeciesType::neutral) { + neutrals.push_back(sp); + } else { + throw BoutException("Species {} has unrecognised type {}", sp, toString(type)); + } + finish_construction(); + } + } + + std::vector electrons, neutrals, positive_ions, negative_ions, ions, charged, non_electrons, all_species; + + private: + void finish_construction() { + ions = positive_ions; + ions.insert(ions.end(), negative_ions.begin(), negative_ions.end()); + charged = ions; + charged.insert(charged.end(), electrons.begin(), electrons.end()); + non_electrons = ions; + non_electrons.insert(non_electrons.end(), neutrals.begin(), neutrals.end()); + all_species = charged; + all_species.insert(all_species.end(), neutrals.begin(), neutrals.end()); + } +}; + +/// Lightweight type to store information used to build a component. +struct ComponentInformation { + std::string name; + std::string type; + + ComponentInformation() {}; + + ComponentInformation(const std::string& name_, const std::string& type_) : name(name_), type(type_) {} + + ComponentInformation(std::string&& name_, std::string&& type_) : name(std::move(name_)), type(std::move(type_)) {} + + bool operator<(const ComponentInformation& other) const { + return std::pair(name, type) < std::pair(other.name, other.type); + } + + bool operator==(const ComponentInformation& other) const { + return std::pair(name, type) == std::pair(other.name, other.type); + } +}; + +/// Format `ComponentInformation` to string. Format string specification is the +/// same as when formatting a string. +/// See https://fmt.dev/12.0/syntax/#format-specification-mini-language. +/// +/// TODO: provide custom formatting to configure exactly how the +/// component name and type are displayed. +template <> +struct fmt::formatter + : formatter { + auto format(const ComponentInformation& ci, format_context& ctx) const + -> format_context::iterator; +}; + + /// Interface for a component of a simulation model -/// +/// /// The constructor of derived types should have signature /// (std::string name, Options &options, Solver *solver) -/// +/// struct Component { + /// Initialise the `state_variable_acceess` permissions. Note that + /// `{all_species}` in any variable names will be replaced with the + /// names of all species being simulated (by calling + /// `declareAllSpecies()`, which is done after all components are + /// created by a ComponentSchedular). + Component(Permissions&& access_permissions) + : state_variable_access(access_permissions) {} + virtual ~Component() {} - /// Modify the given simulation state - /// All components must implement this function - virtual void transform(Options &state) = 0; - + /// Return a list of names/types of other components needed by this + /// component. All configurations for these components will take the + /// default value, unless set in the input file. + virtual std::vector additionalComponents() { return {}; } + + /// Modify the given simulation state. This method will wrap the + /// state in a GuardedOptions object and pass that to the private + /// implementation of transform provided by each component. + void transform(Options& state); + /// Use the final simulation state to update internal state /// (e.g. time derivatives) - virtual void finally(const Options &UNUSED(state)) { } + virtual void finally(const Options& UNUSED(state)) {} /// Add extra fields for output, or set attributes e.g docstrings - virtual void outputVars(Options &UNUSED(state)) { } + virtual void outputVars(Options& UNUSED(state)) {} /// Add extra fields to restart files - virtual void restartVars(Options &UNUSED(state)) { } + virtual void restartVars(Options& UNUSED(state)) {} /// Preconditioning - virtual void precon(const Options &UNUSED(state), BoutReal UNUSED(gamma)) { } - + virtual void precon(const Options& UNUSED(state), BoutReal UNUSED(gamma)) {} + /// Create a Component /// /// @param type The name of the component type to create (e.g. "evolve_density") /// @param name The species/name for this instance. /// @param options Component settings: options[name] are specific to this component /// @param solver Time-integration solver - static std::unique_ptr create(const std::string &type, // The type to create - const std::string &name, // The species/name for this instance - Options &options, // Component settings: options[name] are specific to this component - Solver *solver); // Time integration solver + static std::unique_ptr create(const std::string& type, + const std::string& name, Options& options, + Solver* solver); + + /// Tell the component the name of all species in the simulation, by type. It + /// will use this information to substitute the following placeholders in + /// `state_variable_access`: + /// - electrons (any electron species) + /// - electrons2 (same as above, used for Cartesian product) + /// - neutrals (species with no charge) + /// - neutrals2 (same as above, used for Cartesian product) + /// - positive_ions (ions with a positive charge) + /// - positive_ions2 (same as above, used for Cartesian product) + /// - negative_ions (ions with a negative charge) + /// - negative_ions2 (same as above, used for Cartesian product) + /// - ions (all ions, regardless of sign of charge) + /// - ions2 (same as above, used for Cartesian product) + /// - charged (ions and electrons) + /// - charged2 (same as above, used for Cartesian product) + /// - non_electrons (ions and neutrals) + /// - non_electrons2 (same as above, used for Cartesian product) + /// - all_species (ions, neutrals, and electrons) + /// - all_species2 (same as above, used for Cartesian product) + /// + /// At the end of this function there is a call to + /// Permissions::checkNoRemainingSubstitutions. All substitutions + /// must be completed or else an exception will be thrown. + void declareAllSpecies(const SpeciesInformation & info); + + const Permissions& getPermissions() const { return state_variable_access; } + +protected: + /// Set the level of access needed by this component for a particular variable. + void setPermissions(const std::string& variable, + const Permissions::AccessRights& rights) { + state_variable_access.setAccess(variable, rights); + } + void setPermissions(const Permissions::VarRights& info) { + setPermissions(info.name, info.rights); + } + + /// Replace a placeholder in the name of variables stored in the access control + /// information for this component. + void substitutePermissions(const std::string& label, + const std::vector& substitution) { + state_variable_access.substitute(label, substitution); + } + +private: + /// Information on which state variables the transform method will read and write. + Permissions state_variable_access; + + /// The implementation of the transform method. Modify the given + /// simulation state. All components must implement this + /// function. It will only allow the reading from/writing to state + /// variables with the appropriate permissiosn in + /// `state_variable_access`. + virtual void transform_impl(GuardedOptions &state) = 0; }; /////////////////////////////////////////////////////////////////// @@ -96,11 +259,21 @@ T getNonFinal(const Options& option) { option.str(), typeid(T).name()); } } +template +T getNonFinal(const GuardedOptions & option) { + return getNonFinal(option.get()); +} #define TOSTRING_(x) #x #define TOSTRING(x) TOSTRING_(x) +namespace hermes { +/// Enable a function if and only if `T` is a (subclass of) `GuardedOptions` +template +using EnableIfGuardedOption = std::enable_if_t>; +} + /// Faster non-printing getter for Options /// If this fails, it will throw BoutException /// @@ -120,11 +293,16 @@ T get(const Options& option, [[maybe_unused]] const std::string& location = "") #endif return getNonFinal(option); } +template +T get(const GuardedOptions & option, const std::string& location = "") { + return get(option.get(), location); +} /// Check if an option can be fetched /// Sets the final flag so setting the value /// afterwards will lead to an error bool isSetFinal(const Options& option, const std::string& location = ""); +bool isSetFinal(const GuardedOptions & option, const std::string& location = ""); #if CHECKLEVEL >= 1 /// A wrapper around isSetFinal() which captures debugging information @@ -142,6 +320,7 @@ bool isSetFinal(const Options& option, const std::string& location = ""); /// Sets the final flag so setting the value in the domain /// afterwards will lead to an error bool isSetFinalNoBoundary(const Options& option, const std::string& location = ""); +bool isSetFinalNoBoundary(const GuardedOptions & option, const std::string& location = ""); #if CHECKLEVEL >= 1 /// A wrapper around isSetFinalNoBoundary() which captures debugging information @@ -188,6 +367,11 @@ T getNoBoundary(const Options& option, [[maybe_unused]] const std::string& locat return getNonFinal(option); } +template> +T getNoBoundary(GO&& option, const std::string& location = "") { + return getNoBoundary(std::forward(option).get(Regions::Interior), location); +} + #if CHECKLEVEL >= 1 /// A wrapper around get<>() which captures debugging information /// @@ -261,6 +445,12 @@ Options& set(Options& option, T value) { return option; } +template> +decltype(auto) set(GO&& option, T value) { + set(std::forward(option).getWritable(), value); + return std::forward(option); +} + /// Set values in an option. This could be optimised, but /// currently the is_value private variable would need to be modified. /// @@ -282,6 +472,12 @@ Options& setBoundary(Options& option, T value) { return option; } +template> +decltype(auto) setBoundary(GO&& option, T value) { + setBoundary(std::forward(option).getWritable(Regions::Boundaries), value); + return std::forward(option); +} + /// Add value to a given option. If not already set, treats /// as zero and sets the option to the value. /// @@ -294,17 +490,24 @@ template Options& add(Options& option, T value) { if (!option.isSet()) { return set(option, value); - } else { - try { - return set(option, value + bout::utils::variantStaticCastOrThrow(option.value)); - } catch (const std::bad_cast &e) { - // Convert to a more useful error message - throw BoutException("Could not convert {:s} to type {:s}", - option.str(), typeid(T).name()); - } + } + try { + return set(option, value + + bout::utils::variantStaticCastOrThrow( + option.value)); + } catch (const std::bad_cast& e) { + // Convert to a more useful error message + throw BoutException("Could not convert {:s} to type {:s}", option.str(), + typeid(T).name()); } } +template> +decltype(auto) add(GO&& option, T value) { + add(std::forward(option).getWritable(), value); + return std::forward(option); +} + /// Add value to a given option. If not already set, treats /// as zero and sets the option to the value. /// @@ -314,15 +517,22 @@ template Options& subtract(Options& option, T value) { if (!option.isSet()) { return set(option, -value); - } else { - try { - return set(option, bout::utils::variantStaticCastOrThrow(option.value) - value); - } catch (const std::bad_cast &e) { - // Convert to a more useful error message - throw BoutException("Could not convert {:s} to type {:s}", - option.str(), typeid(T).name()); - } } + try { + return set(option, + bout::utils::variantStaticCastOrThrow(option.value) + - value); + } catch (const std::bad_cast& e) { + // Convert to a more useful error message + throw BoutException("Could not convert {:s} to type {:s}", option.str(), + typeid(T).name()); + } +} + +template> +decltype(auto) subtract(GO&& option, T value) { + subtract(std::forward(option).getWritable(), value); + return std::forward(option); } template @@ -331,6 +541,11 @@ void set_with_attrs(Options& option, T value, std::initializer_list> +void set_with_attrs(GO&& option, T value, std::initializer_list> attrs) { + set_with_attrs(std::forward(option).getWritable(), value, attrs); +} + #if CHECKLEVEL >= 1 template<> inline void set_with_attrs(Options& option, Field3D value, std::initializer_list> attrs) { @@ -340,6 +555,11 @@ inline void set_with_attrs(Options& option, Field3D value, std::initializer_list option.force(value); option.setAttributes(attrs); } + +template> +inline void set_with_attrs(GO&& option, Field3D value, std::initializer_list> attrs) { + set_with_attrs(std::forward(option).getWritable(), std::move(value), attrs); +} #endif #endif // HERMES_COMPONENT_H diff --git a/include/component_scheduler.hxx b/include/component_scheduler.hxx index 2f93904ff..c02cbd2ad 100644 --- a/include/component_scheduler.hxx +++ b/include/component_scheduler.hxx @@ -1,13 +1,16 @@ - #pragma once - #ifndef COMPONENT_SCHEDULER_H #define COMPONENT_SCHEDULER_H -#include "component.hxx" - -#include #include +#include +#include +#include + +#include +#include + +#include "component.hxx" /// Creates and schedules model components /// @@ -55,6 +58,11 @@ public: /// Preconditioning void precon(const Options &state, BoutReal gamma); + + /// All the variable names which are pre-set in the state, before + /// any components are applied. + static const std::set predeclared_variables; + private: /// The components to be executed in order std::vector> components; diff --git a/include/detachment_controller.hxx b/include/detachment_controller.hxx index 77c39465b..58ee5ad8d 100644 --- a/include/detachment_controller.hxx +++ b/include/detachment_controller.hxx @@ -9,8 +9,11 @@ struct DetachmentController : public Component { - DetachmentController(std::string, Options& options, Solver*) { -ASSERT0(BoutComm::size() == 1); // Only works on one processor + DetachmentController(std::string, Options& options, Solver*) + : Component({readOnly("species:{neutral}:density", Regions::Interior), + readOnly("species:e:density", Regions::Interior), readOnly("time"), + readWrite("species:{sp}:{output}")}) { + ASSERT0(BoutComm::size() == 1); // Only works on one processor Options& detachment_controller_options = options["detachment_controller"]; const auto& units = options["units"]; @@ -153,10 +156,20 @@ ASSERT0(BoutComm::size() == 1); // Only works on one processor detachment_controller_options["debug"] .doc("Print debugging information to the screen (0 for none, 1 for basic, 2 for extensive).") .withDefault(0); - - }; - void transform(Options& state) override; + substitutePermissions("neutral", {neutral_species}); + substitutePermissions( + "sp", std::vector(species_list.begin(), species_list.end())); + std::string output; + if (control_mode == control_power) { + output = "energy_source"; + } else if (control_mode == control_particles) { + output = "density_source"; + } else { + ASSERT2(false); + } + substitutePermissions("output", {output}); + }; void outputVars(Options& state) override { AUTO_TRACE(); @@ -316,6 +329,8 @@ ASSERT0(BoutComm::size() == 1); // Only works on one processor std::vector time_buffer; std::vector error_buffer; + void transform_impl(GuardedOptions& state) override; + }; namespace { diff --git a/include/diamagnetic_drift.hxx b/include/diamagnetic_drift.hxx index 2a4bbde82..0ca0d8481 100644 --- a/include/diamagnetic_drift.hxx +++ b/include/diamagnetic_drift.hxx @@ -9,6 +9,11 @@ struct DiamagneticDrift : public Component { DiamagneticDrift(std::string name, Options &options, Solver *UNUSED(solver)); +private: + Vector2D Curlb_B; + bool bndry_flux; + Field2D diamag_form; + /// For every species, if it has: /// - temperature /// - charge @@ -17,12 +22,7 @@ struct DiamagneticDrift : public Component { /// - density_source /// - energy_source /// - momentum_source - void transform(Options &state) override; - -private: - Vector2D Curlb_B; - bool bndry_flux; - Field2D diamag_form; + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/electromagnetic.hxx b/include/electromagnetic.hxx index 4296a7a5e..aacd11beb 100644 --- a/include/electromagnetic.hxx +++ b/include/electromagnetic.hxx @@ -37,24 +37,6 @@ struct Electromagnetic : public Component { /// Electromagnetic(std::string name, Options &options, Solver *solver); - /// Inputs - /// - species - /// - <..> All species with charge and parallel momentum - /// - charge - /// - momentum - /// - density - /// - AA - /// - /// Sets - /// - species - /// - <..> All species with charge and parallel momentum - /// - momentum (modifies) to m n v|| - /// - velocity (modifies) to v|| - /// - fields - /// - Apar Electromagnetic potential - /// - void transform(Options &state) override; - // Save and restore Apar from restart files void restartVars(Options& state) override; @@ -75,6 +57,24 @@ private: Field3D Apar_flutter; bool diagnose; ///< Output additional diagnostics? + + /// Inputs + /// - species + /// - <..> All species with charge and parallel momentum + /// - charge + /// - momentum + /// - density + /// - AA + /// + /// Sets + /// - species + /// - <..> All species with charge and parallel momentum + /// - momentum (modifies) to m n v|| + /// - velocity (modifies) to v|| + /// - fields + /// - Apar Electromagnetic potential + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/electron_force_balance.hxx b/include/electron_force_balance.hxx index 6fc771881..02ec096ed 100644 --- a/include/electron_force_balance.hxx +++ b/include/electron_force_balance.hxx @@ -19,7 +19,16 @@ /// components which impose forces on electrons /// struct ElectronForceBalance : public Component { - ElectronForceBalance(std::string name, Options& alloptions, Solver*) { + ElectronForceBalance(std::string name, Options& alloptions, Solver*) + : Component({readOnly("species:e:pressure"), + readOnly("species:e:density", Regions::Interior), + readOnly("species:e:charge"), + // FIXME: Only writes if already exists + readIfSet("species:e:momentum_source"), + readIfSet("species:{non_electrons}:density", Regions::Interior), + readIfSet("species:{non_electrons}:charge"), + // FIXME: Only written if density and charge have been set. + readWrite("species:{non_electrons}:momentum_source")}) { AUTO_TRACE(); auto& options = alloptions[name]; diagnose = options["diagnose"] @@ -27,6 +36,13 @@ struct ElectronForceBalance : public Component { .withDefault(false); } + /// Save output diagnostics + void outputVars(Options& state) override; +private: + bool diagnose; ///< Output additional fields + + Field3D Epar; ///< Parallel electric field + /// Required inputs /// - species /// - e @@ -40,14 +56,7 @@ struct ElectronForceBalance : public Component { /// - if both density and charge are set /// - momentum_source /// - void transform(Options &state) override; - - /// Save output diagnostics - void outputVars(Options& state) override; -private: - bool diagnose; ///< Output additional fields - - Field3D Epar; ///< Parallel electric field + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/evolve_density.hxx b/include/evolve_density.hxx index c2a7c15cc..273d1999d 100644 --- a/include/evolve_density.hxx +++ b/include/evolve_density.hxx @@ -32,14 +32,6 @@ struct EvolveDensity : public Component { /// EvolveDensity(std::string name, Options &options, Solver *solver); - /// This sets in the state - /// - species - /// - - /// - AA - /// - charge - /// - density - void transform(Options &state) override; - /// Calculate ddt(N). /// /// Requires state components @@ -93,6 +85,16 @@ private: bool diagnose; ///< Output additional diagnostics? Field3D flow_xlow, flow_ylow; ///< Particle flow diagnostics + + /// This sets in the state + /// - species + /// - + /// - AA + /// - charge (if non-zero) + /// - density + /// - density_source + /// - low_n_coeff (if low_n_diffuse) + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/evolve_energy.hxx b/include/evolve_energy.hxx index 440a23ef0..441c0d128 100644 --- a/include/evolve_energy.hxx +++ b/include/evolve_energy.hxx @@ -36,20 +36,6 @@ struct EvolveEnergy : public Component { /// EvolveEnergy(std::string name, Options& options, Solver* solver); - /// Inputs - /// - species - /// - - /// - density - /// - velocity - /// - /// Sets - /// - species - /// - - /// - pressure - /// - temperature - /// - void transform(Options& state) override; - /// /// Optional inputs /// @@ -97,6 +83,21 @@ private: bool diagnose; ///< Output additional diagnostics? bool enable_precon; ///< Enable preconditioner? Field3D flow_xlow, flow_ylow; ///< Energy flow diagnostics + + /// Inputs + /// - species + /// - + /// - AA + /// - density + /// - velocity + /// + /// Sets + /// - species + /// - + /// - pressure + /// - temperature + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/evolve_momentum.hxx b/include/evolve_momentum.hxx index c13f91571..02711488a 100644 --- a/include/evolve_momentum.hxx +++ b/include/evolve_momentum.hxx @@ -7,13 +7,6 @@ /// Evolve parallel momentum struct EvolveMomentum : public Component { EvolveMomentum(std::string name, Options &options, Solver *solver); - - /// This sets in the state - /// - species - /// - - /// - momentum - /// - velocity if density is defined - void transform(Options &state) override; /// Calculate ddt(NV). /// @@ -54,6 +47,19 @@ private: bool diagnose; ///< Output additional diagnostics? bool fix_momentum_boundary_flux; ///< Fix momentum flux to boundary condition? Field3D flow_xlow, flow_ylow; ///< Momentum flow diagnostics + + /// This takes as inputs + /// - species + /// - + /// - AA + /// - density + /// + /// This sets in the state + /// - species + /// - + /// - momentum + /// - velocity if density is defined + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/evolve_pressure.hxx b/include/evolve_pressure.hxx index 04fbe719d..32b2ef9ba 100644 --- a/include/evolve_pressure.hxx +++ b/include/evolve_pressure.hxx @@ -38,19 +38,6 @@ struct EvolvePressure : public Component { /// EvolvePressure(std::string name, Options& options, Solver* solver); - /// Inputs - /// - species - /// - - /// - density - /// - /// Sets - /// - species - /// - - /// - pressure - /// - temperature Requires density - /// - void transform(Options& state) override; - /// /// Optional inputs /// @@ -113,6 +100,19 @@ private: bool fix_momentum_boundary_flux; ///< Fix momentum flux to boundary condition? Field3D Sp_nvh; ///< Pressure source due to artificial viscosity Field3D E_PdivV, E_VgradP; ///< Diagnostic energy source terms for p*Div(V) and V*Grad(P) + + /// Inputs + /// - species + /// - + /// - density + /// + /// Sets + /// - species + /// - + /// - pressure + /// - temperature Requires density + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/fixed_density.hxx b/include/fixed_density.hxx index 0ee9080d3..41a937a8d 100644 --- a/include/fixed_density.hxx +++ b/include/fixed_density.hxx @@ -13,7 +13,7 @@ struct FixedDensity : public Component { /// - charge /// - density value (expression) in units of m^-3 FixedDensity(std::string name, Options& alloptions, Solver* UNUSED(solver)) - : name(name) { + : Component({readWrite("species:{name}:{vars}")}), name(name) { AUTO_TRACE(); auto& options = alloptions[name]; @@ -27,23 +27,8 @@ struct FixedDensity : public Component { // Get the density and normalise N = options["density"].as() / Nnorm; - } - - /// Sets in the state the density, mass and charge of the species - /// - /// - species - /// - - /// - AA - /// - charge - /// - density - void transform(Options& state) override { - AUTO_TRACE(); - auto& species = state["species"][name]; - if (charge != 0.0) { // Don't set charge for neutral species - set(species["charge"], charge); - } - set(species["AA"], AA); // Atomic mass - set(species["density"], N); + substitutePermissions("name", {name}); + substitutePermissions("vars", {"AA", "charge", "density"}); } void outputVars(Options& state) override { @@ -66,6 +51,23 @@ private: BoutReal AA; ///< Atomic mass e.g. proton = 1 Field3D N; ///< Species density (normalised) + + /// Sets in the state the density, mass and charge of the species + /// + /// - species + /// - + /// - AA + /// - charge + /// - density + void transform_impl(GuardedOptions& state) override { + AUTO_TRACE(); + auto species = state["species"][name]; + if (charge != 0.0) { // Don't set charge for neutral species + set(species["charge"], charge); + } + set(species["AA"], AA); // Atomic mass + set(species["density"], N); + } }; namespace { diff --git a/include/fixed_fraction_ions.hxx b/include/fixed_fraction_ions.hxx index 5ea0b5f1b..b43291922 100644 --- a/include/fixed_fraction_ions.hxx +++ b/include/fixed_fraction_ions.hxx @@ -13,6 +13,9 @@ struct FixedFractionIons : public Component { /// e.g. 'd+ @ 0.5, t+ @ 0.5' FixedFractionIons(std::string name, Options &options, Solver *UNUSED(solver)); + private: + std::vector> fractions; + /// Required inputs /// /// - species @@ -25,10 +28,7 @@ struct FixedFractionIons : public Component { /// - /// - density = * electron density /// - ... - void transform(Options &state) override; - - private: - std::vector> fractions; + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/fixed_fraction_radiation.hxx b/include/fixed_fraction_radiation.hxx index bba24d01c..bb48a549b 100644 --- a/include/fixed_fraction_radiation.hxx +++ b/include/fixed_fraction_radiation.hxx @@ -345,7 +345,10 @@ struct FixedFractionRadiation : public Component { /// Inputs /// - /// - fraction - FixedFractionRadiation(std::string name, Options &alloptions, Solver *UNUSED(solver)) : name(name) { + FixedFractionRadiation(std::string name, Options& alloptions, Solver* UNUSED(solver)) + : Component({readOnly("species:e:{inputs}", Regions::Interior), + readWrite("species:e:energy_source")}), + name(name) { auto& options = alloptions[name]; fraction = options["fraction"] @@ -369,8 +372,37 @@ struct FixedFractionRadiation : public Component { Tnorm = get(units["eV"]); Nnorm = get(units["inv_meters_cubed"]); FreqNorm = 1. / get(units["seconds"]); + + substitutePermissions("inputs", {"density", "temperature"}); + } + + void outputVars(Options& state) override { + AUTO_TRACE(); + + if (diagnose) { + set_with_attrs(state[std::string("R") + name], -radiation, + {{"time_dimension", "t"}, + {"units", "W / m^3"}, + {"conversion", SI::qe * Tnorm * Nnorm * FreqNorm}, + {"long_name", std::string("Radiation cooling ") + name}, + {"source", "fixed_fraction_radiation"}}); + } } + private: + std::string name; + + CoolingCurve cooling; ///< The cooling curve L(T) -> Wm^3 + BoutReal fraction; ///< Fixed fraction + + bool diagnose; ///< Output radiation diagnostic? + bool no_core_radiation; ///< Set radiation to zero in core? + BoutReal radiation_multiplier; ///< Scale the radiation rate by this factor + Field3D radiation; ///< For output diagnostic + + // Normalisations + BoutReal Tnorm, Nnorm, FreqNorm; + /// Required inputs /// /// - species @@ -384,8 +416,8 @@ struct FixedFractionRadiation : public Component { /// - e /// - energy_source /// - void transform(Options &state) override { - auto& electrons = state["species"]["e"]; + void transform_impl(GuardedOptions& state) override { + auto electrons = state["species"]["e"]; // Don't need boundary cells const Field3D Ne = GET_NOBOUNDARY(Field3D, electrons["density"]); const Field3D Te = GET_NOBOUNDARY(Field3D, electrons["temperature"]); @@ -421,32 +453,6 @@ struct FixedFractionRadiation : public Component { // Remove radiation from the electron energy source subtract(electrons["energy_source"], radiation); } - - void outputVars(Options& state) override { - AUTO_TRACE(); - - if (diagnose) { - set_with_attrs(state[std::string("R") + name], -radiation, - {{"time_dimension", "t"}, - {"units", "W / m^3"}, - {"conversion", SI::qe * Tnorm * Nnorm * FreqNorm}, - {"long_name", std::string("Radiation cooling ") + name}, - {"source", "fixed_fraction_radiation"}}); - } - } - private: - std::string name; - - CoolingCurve cooling; ///< The cooling curve L(T) -> Wm^3 - BoutReal fraction; ///< Fixed fraction - - bool diagnose; ///< Output radiation diagnostic? - bool no_core_radiation; ///< Set radiation to zero in core? - BoutReal radiation_multiplier; ///< Scale the radiation rate by this factor - Field3D radiation; ///< For output diagnostic - - // Normalisations - BoutReal Tnorm, Nnorm, FreqNorm; }; namespace { diff --git a/include/fixed_temperature.hxx b/include/fixed_temperature.hxx index 19c8ec6cb..ae4a513bc 100644 --- a/include/fixed_temperature.hxx +++ b/include/fixed_temperature.hxx @@ -11,7 +11,11 @@ struct FixedTemperature : public Component { /// - /// - temperature value (expression) in units of eV FixedTemperature(std::string name, Options& alloptions, Solver* UNUSED(solver)) - : name(name) { + : Component({readIfSet("species:{name}:density", Regions::Interior), + readWrite("species:{name}:temperature"), + // FIXME: Only written if density is set + readWrite("species:{name}:pressure")}), + name(name) { AUTO_TRACE(); auto& options = alloptions[name]; @@ -24,36 +28,10 @@ struct FixedTemperature : public Component { / Tnorm; // Normalise diagnose = options["diagnose"] - .doc("Save additional output diagnostics") - .withDefault(false); - } - - /// Sets in the state the temperature and pressure of the species - /// - /// Inputs - /// - species - /// - - /// - density (optional) - /// - /// Sets in the state - /// - /// - species - /// - - /// - temperature - /// - pressure (if density is set) - void transform(Options& state) override { - AUTO_TRACE(); - auto& species = state["species"][name]; + .doc("Save additional output diagnostics") + .withDefault(false); - set(species["temperature"], T); - - // If density is set, also set pressure - if (isSetFinalNoBoundary(species["density"])) { - // Note: The boundary of N may not be set yet - auto N = GET_NOBOUNDARY(Field3D, species["density"]); - P = N * T; - set(species["pressure"], P); - } + substitutePermissions("name", {name}); } void outputVars(Options& state) override { @@ -92,6 +70,34 @@ private: Field3D P; ///< Species pressure (normalised) bool diagnose; ///< Output additional fields + + /// Sets in the state the temperature and pressure of the species + /// + /// Inputs + /// - species + /// - + /// - density (optional) + /// + /// Sets in the state + /// + /// - species + /// - + /// - temperature + /// - pressure (if density is set) + void transform_impl(GuardedOptions& state) override { + AUTO_TRACE(); + auto species = state["species"][name]; + + set(species["temperature"], T); + + // If density is set, also set pressure + if (isSetFinalNoBoundary(species["density"])) { + // Note: The boundary of N may not be set yet + auto N = GET_NOBOUNDARY(Field3D, species["density"]); + P = N * T; + set(species["pressure"], P); + } + } }; namespace { diff --git a/include/fixed_velocity.hxx b/include/fixed_velocity.hxx index cb9613ce7..1113b196f 100644 --- a/include/fixed_velocity.hxx +++ b/include/fixed_velocity.hxx @@ -10,7 +10,10 @@ struct FixedVelocity : public Component { FixedVelocity(std::string name, Options& alloptions, Solver* UNUSED(solver)) - : name(name) { + : Component({readIfSet("species:{name}:density", Regions::Interior), + // FIXME: AA is only read if density is set + readOnly("species:{name}:AA"), readWrite("species:{name}:{output}")}), + name(name) { AUTO_TRACE(); auto& options = alloptions[name]; @@ -28,25 +31,9 @@ struct FixedVelocity : public Component { // Option overrides mesh value // so use mesh value (if any) as default value. V = options["velocity"].withDefault(V) / Cs0; - } - - /// This sets in the state - /// - species - /// - - /// - velocity - /// - momentum - void transform(Options& state) override { - AUTO_TRACE(); - auto& species = state["species"][name]; - set(species["velocity"], V); - - // If density is set, also set momentum - if (isSetFinalNoBoundary(species["density"])) { - const Field3D N = getNoBoundary(species["density"]); - const BoutReal AA = get(species["AA"]); // Atomic mass - - set(species["momentum"], AA * N * V); - } + substitutePermissions("name", {name}); + // FIXME: Momentum is only written if density is set + substitutePermissions("output", {"velocity", "momentum"}); } void outputVars(Options& state) override { @@ -67,6 +54,25 @@ private: std::string name; ///< Short name of species e.g "e" Field3D V; ///< Species velocity (normalised) + + /// This sets in the state + /// - species + /// - + /// - velocity + /// - momentum + void transform_impl(GuardedOptions& state) override { + AUTO_TRACE(); + auto species = state["species"][name]; + set(species["velocity"], V); + + // If density is set, also set momentum + if (isSetFinalNoBoundary(species["density"])) { + const Field3D N = getNoBoundary(species["density"]); + const BoutReal AA = get(species["AA"]); // Atomic mass + + set(species["momentum"], AA * N * V); + } + } }; namespace { diff --git a/include/guarded_options.hxx b/include/guarded_options.hxx new file mode 100644 index 000000000..961ba5d7b --- /dev/null +++ b/include/guarded_options.hxx @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "permissions.hxx" + +/// A wrapper class around BOUT++ Options objects. It uses access +/// right data, stored using a Permissions object, to control reading +/// from and writing to the underlying data. +class GuardedOptions { +public: + GuardedOptions() = delete; + + /// Create a guarded options object which applies the specified + /// permissions to the underlying options object. Note that the + /// variable names used in the Permissions object must always be the + /// full names, relative to the highest-level of the Options + /// hierarchy. + GuardedOptions(Options* options, Permissions* permissions); + + /// Get a subsection or value. The result will also be wrapped in a + /// GuardedOptions object, with the same permissions as this one. + GuardedOptions operator[](const std::string& name) const { + return GuardedOptions(&(*options)[name], permissions, unread_variables, + unwritten_variables); + } + GuardedOptions operator[](const char* name) const { return (*this)[std::string(name)]; } + + std::map getChildren(); + bool isSection(const std::string& name) const { return options->isSection(name); } + bool isSection(const char* name) const { return (*this).isSection(std::string(name)); } + bool isSection() const { return options->isSection(); } + bool isSet(const std::string& name) const { return options->isSet(name); } + bool isSet(const char* name) const { return (*this).isSet((std::string(name))); } + bool isSet() const { return options->isSet(); } + std::string name() const { return options->name(); } + + /// Get read-only access to the underlying Options object. Throws + /// BoutException if there is not read-permission for this object. + const Options& get(Regions region = Regions::All) const; + /// Get read-write access to the underlying Options object. Throws + /// BoutException if there is not write-permission for this object. + Options& getWritable(Regions region = Regions::All); + + /// Returns a list of variables with read-only permission but which + /// have not been accessed using the `get()` method. + std::map unreadItems() const { +#if CHECKLEVEL >= 1 + return *unread_variables; +#else + throw BoutException( + "Reading of items in GuardedOptions is not tracked when CHECKLEVEL < 1"); +#endif + } + + /// Returns a list of variables with read-write permission but which + /// have not been accessed using the `getWritable()` method. + std::map unwrittenItems() const { +#if CHECKLEVEL >= 1 + return *unwritten_variables; +#else + throw BoutException( + "Reading of items in GuardedOptions is not tracked when CHECKLEVEL < 1"); +#endif + } + + bool operator==(const GuardedOptions& other) const; + bool operator!=(const GuardedOptions& other) const; + + PermissionTypes getHighestPermission(Regions region = Regions::All) const { + return permissions->getHighestPermission(options->str(), region).first; + } + +private: + Options* options; + Permissions* permissions; + mutable std::shared_ptr> unread_variables, + unwritten_variables; + + GuardedOptions(Options* options, Permissions* permissions, + std::shared_ptr> unread_vars, + std::shared_ptr> unwritten_vars) + : options(std::move(options)), permissions(std::move(permissions)), + unread_variables(std::move(unread_vars)), + unwritten_variables(std::move(unwritten_vars)) {} +}; diff --git a/include/hydrogen_charge_exchange.hxx b/include/hydrogen_charge_exchange.hxx index 0cf1ed390..44057c128 100644 --- a/include/hydrogen_charge_exchange.hxx +++ b/include/hydrogen_charge_exchange.hxx @@ -29,8 +29,13 @@ struct HydrogenChargeExchange : public ReactionBase { /// - eV /// - inv_meters_cubed /// - seconds - HydrogenChargeExchange([[maybe_unused]] std::string name, Options& alloptions, - Solver*) { + HydrogenChargeExchange([[maybe_unused]] std::string name, Options& alloptions, Solver*) + : ReactionBase({readOnly("species:{reactant}:{react_vals}"), + readOnly("species:{sp}:{read_vals}"), + readWrite("species:{sp}:{writevals}"), + readWrite("species:{reactant}:collision_frequency"), + readWrite("species:{atom}:collision_frequencies:{atom}_{ion}_cx"), + readWrite("species:{ion}:collision_frequencies:{ion}_{atom}_cx")}) { // Get the units const auto& units = alloptions["units"]; Tnorm = get(units["eV"]); @@ -49,11 +54,11 @@ protected: /// /// atom1 -> ion2, ion1 -> atom2 /// - /// Assumes that both atom1 and ion1 have: - /// - AA - /// - density - /// - velocity - /// - temperature + /// Assumes that species have: + /// - AA (all) + /// - density (atom1, ion1) + /// - velocity (all) + /// - temperature (atom2, ion2) /// /// Sets in all species: /// - density_source [If atom1 != atom2 or ion1 != ion2] @@ -69,7 +74,7 @@ protected: /// atom_energy Energy removed from atom1, added to ion2 /// ion_energy Energy removed from ion1, added to atom2 /// - void calculate_rates(Options& atom1, Options& ion1, Options& atom2, Options& ion2, + void calculate_rates(GuardedOptions&& atom1, GuardedOptions&& ion1, GuardedOptions&& atom2, GuardedOptions&& ion2, Field3D& R, Field3D& atom_mom, Field3D& ion_mom, Field3D& atom_energy, Field3D& ion_energy, Field3D& atom_rate, Field3D& ion_rate, BoutReal& rate_multiplier, @@ -138,38 +143,19 @@ struct HydrogenIsotopeChargeExchange : public HydrogenChargeExchange { rate_multiplier = alloptions[{Isotope1}]["K_cx_multiplier"] .doc("Scale the charge exchange rate by this factor") .withDefault(1.0); - } - - void transform(Options& state) override { - Field3D R, atom_mom, ion_mom, atom_energy, ion_energy; - calculate_rates(state["species"][{Isotope1}], // e.g. "h" - state["species"][{Isotope2, '+'}], // e.g. "d+" - state["species"][{Isotope2}], // e.g. "d" - state["species"][{Isotope1, '+'}], // e.g. "h+" - R, atom_mom, ion_mom, atom_energy, ion_energy, // Transfer channels - atom_rate, ion_rate, // Collision rates in s^-1 - rate_multiplier, // Arbitrary user set multiplier - no_neutral_cx_mom_gain); // Make CX behave as in diffusive neutrals? - - if (diagnose) { - // Calculate diagnostics to be written to dump file - if (Isotope1 == Isotope2) { - // Simpler case of same isotopes - // - No net particle source/sink - // - atoms lose atom_mom, gain ion_mom - // - F = ion_mom - atom_mom; // Momentum transferred to atoms due to CX with ions - E = ion_energy - atom_energy; // Energy transferred to atoms - } else { - // Different isotopes - S = -R; // Source of Isotope1 atoms - F = -atom_mom; // Source of momentum for Isotope1 atoms - F2 = -ion_mom; // Source of momentum for Isotope2 ions - E = -atom_energy; // Source of energy for Isotope1 atoms - E2 = -ion_energy; // Source of energy for Isotope2 ions - } + std::vector writevals = {"momentum_source", "energy_source"}; + if constexpr (Isotope1 != Isotope2) { + writevals.push_back("density_source"); } + substitutePermissions("reactant", {{Isotope1}, {Isotope2, '+'}}); + substitutePermissions("react_vals", {"density", "temperature"}); + substitutePermissions("read_vals", {"AA", "velocity"}); + substitutePermissions("sp", + {{Isotope1}, {Isotope2, '+'}, {Isotope1, '+'}, {Isotope2}}); + substitutePermissions("writevals", writevals); + substitutePermissions("atom", {{Isotope1}}); + substitutePermissions("ion", {{Isotope2, '+'}}); } void outputVars(Options& state) override { @@ -267,6 +253,38 @@ private: Field3D E, E2; ///< Energy exchange Field3D atom_rate, ion_rate; ///< Collision rates in s^-1 bool no_neutral_cx_mom_gain; ///< Make CX behave as in diffusive neutrals? + + void transform_impl(GuardedOptions& state) override { + Field3D R, atom_mom, ion_mom, atom_energy, ion_energy; + + calculate_rates(state["species"][{Isotope1}], // e.g. "h" + state["species"][{Isotope2, '+'}], // e.g. "d+" + state["species"][{Isotope2}], // e.g. "d" + state["species"][{Isotope1, '+'}], // e.g. "h+" + R, atom_mom, ion_mom, atom_energy, ion_energy, // Transfer channels + atom_rate, ion_rate, // Collision rates in s^-1 + rate_multiplier, // Arbitrary user set multiplier + no_neutral_cx_mom_gain); // Make CX behave as in diffusive neutrals? + + if (diagnose) { + // Calculate diagnostics to be written to dump file + if (Isotope1 == Isotope2) { + // Simpler case of same isotopes + // - No net particle source/sink + // - atoms lose atom_mom, gain ion_mom + // + F = ion_mom - atom_mom; // Momentum transferred to atoms due to CX with ions + E = ion_energy - atom_energy; // Energy transferred to atoms + } else { + // Different isotopes + S = -R; // Source of Isotope1 atoms + F = -atom_mom; // Source of momentum for Isotope1 atoms + F2 = -ion_mom; // Source of momentum for Isotope2 ions + E = -atom_energy; // Source of energy for Isotope1 atoms + E2 = -ion_energy; // Source of energy for Isotope2 ions + } + } + } }; namespace { diff --git a/include/ionisation.hxx b/include/ionisation.hxx index d3a0e0dfd..6c464d251 100644 --- a/include/ionisation.hxx +++ b/include/ionisation.hxx @@ -8,12 +8,13 @@ class Ionisation : public Component { public: Ionisation(std::string name, Options &options, Solver *); - void transform(Options &state) override; private: BoutReal Eionize; // Energy loss per ionisation [eV] BoutReal Tnorm, Nnorm, FreqNorm; // Normalisations + + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/isothermal.hxx b/include/isothermal.hxx index f80ee2656..1b385c5bd 100644 --- a/include/isothermal.hxx +++ b/include/isothermal.hxx @@ -9,6 +9,15 @@ struct Isothermal : public Component { Isothermal(std::string name, Options &options, Solver *); + void outputVars(Options &state) override; +private: + std::string name; // Species name + + BoutReal T; ///< The normalised temperature + Field3D P; ///< The normalised pressure + + bool diagnose; ///< Output additional diagnostics? + /// Inputs /// - species /// - @@ -21,16 +30,7 @@ struct Isothermal : public Component { /// - temperature /// - pressure (if density is set) /// - void transform(Options &state) override; - - void outputVars(Options &state) override; -private: - std::string name; // Species name - - BoutReal T; ///< The normalised temperature - Field3D P; ///< The normalised pressure - - bool diagnose; ///< Output additional diagnostics? + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/neutral_boundary.hxx b/include/neutral_boundary.hxx index f0f1f0d8a..fda96dff2 100644 --- a/include/neutral_boundary.hxx +++ b/include/neutral_boundary.hxx @@ -23,18 +23,6 @@ struct NeutralBoundary : public Component { NeutralBoundary(std::string name, Options& options, Solver*); - /// - /// state - /// - species - /// - - /// - density Free boundary - /// - temperature Free boundary - /// - pressure Free boundary - /// - velocity [if set] Zero boundary - /// - momentum [if set] Zero boundary - /// - energy_source Adds wall losses - /// - void transform(Options& state) override; void outputVars(Options &state) override; private: @@ -53,6 +41,19 @@ private: bool upper_y; ///< Boundary condition at upper y? bool sol; ///< Boundary condition at sol? bool pfr; ///< Boundary condition at pfr? + + /// + /// state + /// - species + /// - + /// - density Free boundary + /// - temperature Free boundary + /// - pressure Free boundary + /// - velocity [if set] Zero boundary + /// - momentum [if set] Zero boundary + /// - energy_source Adds wall losses + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/neutral_full_velocity.hxx b/include/neutral_full_velocity.hxx index e60757bf3..f5f20d81b 100644 --- a/include/neutral_full_velocity.hxx +++ b/include/neutral_full_velocity.hxx @@ -15,9 +15,6 @@ struct NeutralFullVelocity : public Component { NeutralFullVelocity(const std::string& name, Options& options, Solver* solver); - /// Modify the given simulation state - void transform(Options& state) override; - /// Use the final simulation state to update internal state /// (e.g. time derivatives) void finally(const Options& state) override; @@ -65,6 +62,17 @@ private: bool diagnose; ///< Output additional diagnostics? Field2D Vnpar; ///< Parallel flow velocity diagnostic + + /// Sets + /// - species + /// - + /// - AA + /// - density + /// - momentum + /// - pressure + /// - temperature + /// - velocity + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/neutral_mixed.hxx b/include/neutral_mixed.hxx index 0a80aaad6..37979b9a2 100644 --- a/include/neutral_mixed.hxx +++ b/include/neutral_mixed.hxx @@ -19,9 +19,6 @@ struct NeutralMixed : public Component { /// @param solver Time-integration solver to be used NeutralMixed(const std::string& name, Options& options, Solver *solver); - /// Modify the given simulation state - void transform(Options &state) override; - /// Use the final simulation state to update internal state /// (e.g. time derivatives) void finally(const Options &state) override; @@ -82,6 +79,17 @@ private: Field3D mf_visc_perp_xlow, mf_visc_perp_ylow, mf_visc_par_ylow; Field3D ef_adv_perp_xlow, ef_adv_perp_ylow, ef_adv_par_ylow; Field3D ef_cond_perp_xlow, ef_cond_perp_ylow, ef_cond_par_ylow; + + /// Sets + /// - species + /// - + /// - AA + /// - density + /// - momentum + /// - pressure + /// - temperature + /// - velocity + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/neutral_parallel_diffusion.hxx b/include/neutral_parallel_diffusion.hxx index ebabb3ade..9b93f85eb 100644 --- a/include/neutral_parallel_diffusion.hxx +++ b/include/neutral_parallel_diffusion.hxx @@ -23,7 +23,11 @@ /// - F_Dpar Momentum source due to diffusion /// struct NeutralParallelDiffusion : public Component { - NeutralParallelDiffusion(std::string name, Options &alloptions, Solver *) { + NeutralParallelDiffusion(std::string name, Options& alloptions, Solver*) + : Component({readIfSet("species:{all_species}:charge"), + readIfSet("species:{neutrals}:{optional_inputs}"), + readOnly("species:{neutrals}:{inputs}"), + readWrite("species:{neutrals}:{outputs}")}) { auto& options = alloptions[name]; dneut = options["dneut"] .doc("cross-field diffusion projection (B / Bpol)^2") @@ -48,27 +52,15 @@ struct NeutralParallelDiffusion : public Component { perpendicular_viscosity = options["perpendicular_viscosity"] .doc("Enable parallel projection of perpendicular viscosity?") .withDefault(true); - } - /// - /// Inputs - /// - species - /// - # Applies to all neutral species - /// - AA - /// - collision_frequency - /// - density - /// - temperature - /// - pressure [optional, or density * temperature] - /// - velocity [optional] - /// - momentum [if velocity set] - /// - /// Sets - /// - species - /// - - /// - density_source - /// - energy_source - /// - momentum_source [if velocity set] - void transform(Options &state) override; + // FIXME: strictly speaking, momentum is not optional if velocity has been set + substitutePermissions("optional_inputs", {"pressure", "velocity", "momentum"}); + substitutePermissions("inputs", + {"AA", "collision_frequencies", "density", "temperature"}); + // FIXME: momentum_source is only set if velocity was set. + substitutePermissions("outputs", + {"density_source", "energy_source", "momentum_source"}); + } /// Save variables to the output void outputVars(Options &state) override; @@ -93,6 +85,27 @@ private: /// Store diagnostics for each species std::map diagnostics; + + /// + /// Inputs + /// - species + /// - # Applies to all neutral species + /// - AA + /// - charge [if set] + /// - collision_frequencies + /// - density + /// - temperature + /// - pressure [optional, or density * temperature] + /// - velocity [optional] + /// - momentum [if velocity set] + /// + /// Sets + /// - species + /// - + /// - density_source + /// - energy_source + /// - momentum_source [if velocity set] + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/noflow_boundary.hxx b/include/noflow_boundary.hxx index 68d7e91b0..5d19e5749 100644 --- a/include/noflow_boundary.hxx +++ b/include/noflow_boundary.hxx @@ -5,7 +5,8 @@ #include "component.hxx" struct NoFlowBoundary : public Component { - NoFlowBoundary(std::string name, Options& alloptions, Solver*) : name(name) { + NoFlowBoundary(std::string name, Options& alloptions, Solver*) + : Component({writeBoundaryIfSet("species:{name}:{variables}")}), name(name) { AUTO_TRACE(); Options& options = alloptions[name]; @@ -15,8 +16,16 @@ struct NoFlowBoundary : public Component { noflow_upper_y = options["noflow_upper_y"] .doc("No-flow boundary on upper y?") .withDefault(true); + substitutePermissions("name", {name}); + substitutePermissions("variables", + {"density", "temperature", "pressure", "velocity", "momentum"}); } +private: + std::string name; ///< + bool noflow_lower_y; ///< No-flow boundary on lower y? + bool noflow_upper_y; ///< No-flow boundary on upper y? + /// Inputs /// - species /// - @@ -25,12 +34,7 @@ struct NoFlowBoundary : public Component { /// - pressure [Optional] /// - velocity [Optional] /// - momentum [Optional] - void transform(Options& state) override; - -private: - std::string name; ///< - bool noflow_lower_y; ///< No-flow boundary on lower y? - bool noflow_upper_y; ///< No-flow boundary on upper y? + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/permissions.hxx b/include/permissions.hxx new file mode 100644 index 000000000..0d9b7f197 --- /dev/null +++ b/include/permissions.hxx @@ -0,0 +1,364 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/// Ways in which someone is allowed to access the variable, with +/// increasing levels of rights. "ReadIfSet" indicates that +/// variables should only be read if already set. "Final" refers to +/// the last time anyone is allowed to write to the variable. These +/// two concepts need to be captured to decide the order in which to +/// execute components. +enum class PermissionTypes { None = -1, ReadIfSet, Read, Write, Final, END }; + +/// \defgroup RegionsGroup +/// @{ + +/// The regions of the domain to which a particular permission +/// apply. These are designed to be used as bit-flags. +enum class Regions { + Nowhere = 0, + Interior = 1 << 0, + Boundaries = 1 << 1, + All = Interior | Boundaries +}; + +constexpr Regions operator&(Regions a, Regions b) { + return static_cast(static_cast(a) & static_cast(b)); +} + +constexpr Regions operator|(Regions a, Regions b) { + return static_cast(static_cast(a) | static_cast(b)); +} + +constexpr Regions operator~(Regions a) { + return static_cast(~static_cast(a)); +} + +/// @} + +inline auto format_as(Regions r) { return fmt::underlying(r); } + +/// Class to store information on whether particular variables an be +/// read from and/or written to. These permissions can apply on +/// particular regions of the domain. +class Permissions { +public: + Permissions() = default; + Permissions(Permissions& x) = default; + Permissions(Permissions&& x) = default; + Permissions& operator=(Permissions& x) = default; + Permissions& operator=(Permissions&& x) = default; + + const static std::map fundamental_regions; + + /// Data type for storing the regions of a variable which have a + /// particular level of permission. Some examples can be seen below: + /// + /// AccessRights only_read_if_set = { Regions::All, Regions::Nowhere, + /// Regions::Nowhere, Regions::Nowhere }; + /// AccessRights read_only = { Regions::Nowhere, Regions::All, Regions::Nowhere, + /// Regions::Nowhere }; + /// AccessRights write_boundaries = { Regions::Nowhere, Regions::Nowhere, + /// Regions::Boundaries, Regions::Nowhere }; + /// AccessRights read_and_write_everywhere = { Regions::Nowhere, + /// Regions::AllRegions, Regions::All, Regions::Nowhere }; + /// AccessRights final_write_boundaries_read_interior = { Regions::Nowhere, + /// Regions::Interior, Regions::Nowhere, Regions::Boundaries }; + /// + using AccessRights = std::array(PermissionTypes::END)>; + + /// Data used to specify what access rights apply to the named variable. + struct VarRights { + std::string name; + AccessRights rights; + }; + + /// Create permission from an initialiser list. Each item in the + /// initialiser list should be a pair made up of the name of a + /// variable stored in an Options object (with a colon separating + /// section names) and an array describing which regions of the + /// domain each level of permission is applied to. The order of the + /// permissions in the array is: read, write, final write. Note that + /// higher permissions are taken to imply all lower permissions. See + /// the examples below. + /// + /// Permissions example({ + /// // Permission to read charge only if it has been set + /// {"species:he:charge", {Regions::All, Regions::Nowhere, + /// Regions::Nowhere, Regions::Nowhere}}, + /// // Read permission for atomic mass + /// {"species:he:AA", {Regions::Nowhere, Regions::All, + /// Regions::Nowhere, Regions::Nowhere}}, + /// // Read permissions for density + /// {"species:he:density", {Regions::Nowhere, Regions::All, + /// Regions::Nowhere, Regions::Nowhere}}, + /// // Read and write permissions for pressure in the interior region + /// {"species:he:pressure", {Regions::Nowhere, Regions::Nowhere, + /// Regions::Interior, Regions::Nowhere}}, + /// // Set the final value for collision frequency + /// {"species:he:collision_frequency", {Regions::Nowhere, + /// Regions::Nowhere, Regions::Nowhere, Regions::All}} + /// }); + /// + /// If a variable is not included in the initialiser list then it is + /// assumed there are no access rights. If a section name appears in + /// the list then those permissions apply to all children of that + /// section. The section name must end in a colon (e.g., + /// "species:he:"). If an additional entry is present for somethign + /// located in that section, then the more specific entry takes + /// precidences. + /// + /// Placeholders can be used in variable names by surrounding a + /// label with curly braces. Multiple values can then be substituted + /// for this placeholder using the Permissions::substitute + /// method. For example, if you wanted to read the collision + /// frequency for every species you would write: + /// + /// Permissions example2({ + /// {"species:{name}:collision_frequency", {Regions::Nowhere, + /// Regions::All, Regions::Nowhere, Regions::Nowhere}} + /// }); + /// example2.substitute("name", {"he+", "d+", "e", "d", "he"}); + /// + Permissions(std::initializer_list data); + + /// Set the level of access for the various regions of the + /// variable. This uses the same logic as the constructor. For + /// example, to indicate that the density of helium is readable + /// everywhere but only writeable in the interior, you would use + /// + /// permissions.setAccess("species:he:density", + /// {Regions::Nowhere, Regions::All, + /// Regions::Interior, Regions::Nowhere}) + /// + /// or, equivalently, + /// + /// permissions.setAccess("species:he:density", + /// {Regions::Nowhere, Permissions::Boundary, + /// Regions::Interior, Regions::Nowhere}); + /// + /// As in the constructor, if the variable name is just a section in + /// an Options object then the permissions apply to all children of + /// that section. Placeholder names can also be used. + void setAccess(const std::string& variable, const AccessRights& rights); + + void setAccess(const VarRights& info) { setAccess(info.name, info.rights); } + + /// Replace a placeholder in the names of variables stored in this + /// object. This is useful if you need to access the same variable + /// for multiple species. For example, the following code gives + /// permission to read the density and write the collision frequency + /// for every species. + /// + /// Permissions example({ + /// {"species:{name}:density", {Regions::Nowhere, Regions::All, + /// Regions::Nowhere, Regions::Nowhere}}, + /// {"species:{name}:collision_frequency", {Regions::Nowhere, + /// Regions::Nowhere, Regions::All, Regions::Nowhere}}, + /// }); + /// example.substitute("name", {"d", "d+", "t", "t+", "he", "he+", "c", "c+", "e"}); + /// + /// Note that variable names which already have a permission set + /// will not be overwritten. + /// + void substitute(const std::string& label, + const std::vector& substitutions); + + /// Check there are no remaining placeholders that have not been + /// substituted in any variables names. If there are, then throw an + /// exception. + void checkNoRemainingSubstitutions() const; + + /// Check whether users are allowed to access this variable to the + /// given permission level, in the given region. The second item + /// returned indicates the name of the variable or section from + /// which the access rights are derived. If there is no matching + /// section then it will be an empty string. + std::pair + canAccess(const std::string& variable, + PermissionTypes permission = PermissionTypes::Read, + Regions region = Regions::All) const; + + /// Get the highest permission level with which the given variable + /// can be accessed in the given region. The second item returned + /// indicates the name of the variable or section from which the + /// access rights are derived. If there is no matching section then + /// it will return `PermissionTypes::None` and an empty string. + std::pair + getHighestPermission(const std::string& variable, Regions region = Regions::All) const; + + /// Get a set of variables and regions for which there is the + /// specified level of permission to access. If ``highestOnly`` is + /// true then it will only include variables/regions for which this + /// is the highest permission. + /// + /// Permissions example({"test", {Regions::Nowhere, Regions::All, + /// Regions::All, Permissions:Nowhere}}); + /// // Print variables which can be read + /// for (const auto [varname, region] : + /// example.getVariablesWithPermission(PermissionTypes::Read), false)) + /// std::cout << "Variable name: " << varname << ", Region ID: " << region << + /// "\n"; + /// // Print variables which can only be read (not written) + /// for (const auto [varname, region] : + /// example.getVariablesWithPermission(PermissionTypes::Read), true)) + /// std::cout << "Variable name: " << varname << ", Region ID: " << region << + /// "\n"; + /// + /// The above code would write a line of output from the first + /// for-loop, but not the second. + std::map + getVariablesWithPermission(PermissionTypes permission, bool highestOnly = true) const; + + /// Return a string version of the region names + static std::string regionNames(const Regions regions); + + // Allow to be streamed, so can be stored in Options. + friend fmt::formatter; + friend std::ostream& operator<<(std::ostream& os, const Permissions& permissions); + friend std::istream& operator>>(std::istream& is, Permissions& permissions); + + /// Returns the access rights for the most specific entry in this + /// object which matches the variable name. If there are no matching + /// entries then the result will indicate no access rights. The + /// string indicates the name of the variable from which the access + /// rights were derived. It will be empty if there are no matching + /// entries. + VarRights bestMatchRights(const std::string& variable) const; + +private: + std::map variable_permissions; + + static const std::regex LABEL_RE; +}; + +/// Format `Permissions` to string. Format string specification is the +/// same as when formatting a `std::map`. +/// See https://fmt.dev/12.0/syntax/#range-format-specifications. +template <> +struct fmt::formatter + : formatter> { + auto format(const Permissions& p, format_context& ctx) const + -> format_context::iterator; +}; + +/// \defgroup PermissionFactories +/// @{ + +/// Convenience function to return an object expressing that the +/// variable should have ReadIfSet permissions in the specified regions. +inline Permissions::VarRights readIfSet(std::string varname, + Regions region = Regions::All) { + return {std::move(varname), + {region, Regions::Nowhere, Regions::Nowhere, Regions::Nowhere}}; +} + +/// Convenience function to return an object expressing that the +/// variable should have Read permissions in the specified regions. +inline Permissions::VarRights readOnly(std::string varname, + Regions region = Regions::All) { + return {std::move(varname), + {Regions::Nowhere, region, Regions::Nowhere, Regions::Nowhere}}; +} + +/// Convenience function to return an object expressing that the +/// variable should have Write permissions in the specified regions. +inline Permissions::VarRights readWrite(std::string varname, + Regions region = Regions::All) { + return {std::move(varname), + {Regions::Nowhere, Regions::Nowhere, region, Regions::Nowhere}}; +} + +/// Convenience function to return an object expressing that the +/// variable should have Final permissions in the specified regions. +inline Permissions::VarRights writeFinal(std::string varname, + Regions region = Regions::All) { + return {std::move(varname), + {Regions::Nowhere, Regions::Nowhere, Regions::Nowhere, region}}; +} + +/// Convenience function to return an object expressing that the +/// variable should have write permissions on the boundaries. It +/// will have Read permissions in the interior, as this is normally +/// required to set the boundaries correctly. +inline Permissions::VarRights writeBoundary(std::string varname) { + return {std::move(varname), + {Regions::Nowhere, Regions::Interior, Regions::Boundaries, Regions::Nowhere}}; +} + +/// Convenience function to return an object expressing that the +/// variable should have Final write permissions on the boundaries. It +/// will have Read permissions in the interior, as this is normally +/// required to set the boundaries correctly. +inline Permissions::VarRights writeBoundaryFinal(std::string varname) { + return {std::move(varname), + {Regions::Nowhere, Regions::Interior, Regions::Nowhere, Regions::Boundaries}}; +} + +/// Convenience function to return an object expressing that, if the +/// interior has been set, then the variable should have write +/// permissions on the boundaries and read permissions for the +/// interior. +/// +/// FIXME: Currently these permissiosn are not expressed properly, due +/// to limitations in how the permission system. The boundary will +/// have write permission regardless of whether or not the interior is +/// set. +inline Permissions::VarRights writeBoundaryIfSet(std::string varname) { + return {std::move(varname), + {Regions::Interior, Regions::Nowhere, Regions::Boundaries, Regions::Nowhere}}; +} + +/// Convenience function to return an object expressing that, if the +/// interior has been set, then the variable should have Final write +/// permissions on the boundaries and read permissions for the +/// interior. +/// +/// FIXME: Currently these permissiosn are not expressed properly, due +/// to limitations in how the permission system. The boundary will +/// have write permission regardless of whether or not the interior is +/// set. +inline Permissions::VarRights writeBoundaryFinalIfSet(std::string varname) { + return {std::move(varname), + {Regions::Interior, Regions::Nowhere, Regions::Nowhere, Regions::Boundaries}}; +} + +/// Convenience function to return an object expressing that the +/// interior has read-if-set permissions and the boundary has write +/// permissions. This differs from what `writeBoundaryIfSet` +/// is supposed to do because this will write the boundary +/// unconditionally, regardless of whether the interior is set. +inline Permissions::VarRights writeBoundaryReadInteriorIfSet(std::string varname) { + return {std::move(varname), + {Regions::Interior, Regions::Nowhere, Regions::Nowhere, Regions::Boundaries}}; +} +/// @} + +// FIXME: Ideally there would be some way to express write permissions only if set +// FIXME: Ideally we could express to write a boundary only if the interior is set + +/// Write a string-representation of the permissions to an IO +/// stream. This is useful for allowing permission data to be stored +/// in Options objects. +std::ostream& operator<<(std::ostream& os, const Permissions& permissions); + +/// Read a string representation of permissions from an IO +/// stream. This is useful for allowing permissions data to be +/// retrieved from Options objects. Note that there is undefined +/// behaviour if the input is corrupted; an exception may be thrown or +/// the permissions that are read may be incomplete. +std::istream& operator>>(std::istream& is, Permissions& permissions); + +std::string toString(const Permissions& value); diff --git a/include/polarisation_drift.hxx b/include/polarisation_drift.hxx index 0663ad9fa..638bf602b 100644 --- a/include/polarisation_drift.hxx +++ b/include/polarisation_drift.hxx @@ -32,6 +32,23 @@ struct PolarisationDrift : public Component { // PolarisationDrift(std::string name, Options &options, Solver *UNUSED(solver)); + void outputVars(Options &state) override; +private: + std::unique_ptr phiSolver; // Laplacian solver in X-Z + + Field2D Bsq; // Cached SQ(coord->Bxy) + + // Diagnostic outputs + bool diagnose; ///< Save diagnostic outputs? + Field3D DivJ; //< Divergence of all other currents + Field3D phi_pol; //< Polarisation drift potential + + bool boussinesq; // If true, assume a constant mass density in Jpol + BoutReal average_atomic_mass; // If boussinesq=true, mass density to use + BoutReal density_floor; // Minimum mass density if boussinesq=false + bool advection; // Advect fluids by an approximate polarisation velocity? + bool diamagnetic_polarisation; // Calculate compression terms? + /// Inputs /// /// - species @@ -54,24 +71,7 @@ struct PolarisationDrift : public Component { /// - energy_source (if pressure set) /// - momentum_source (if momentum set) /// - void transform(Options &state) override; - - void outputVars(Options &state) override; -private: - std::unique_ptr phiSolver; // Laplacian solver in X-Z - - Field2D Bsq; // Cached SQ(coord->Bxy) - - // Diagnostic outputs - bool diagnose; ///< Save diagnostic outputs? - Field3D DivJ; //< Divergence of all other currents - Field3D phi_pol; //< Polarisation drift potential - - bool boussinesq; // If true, assume a constant mass density in Jpol - BoutReal average_atomic_mass; // If boussinesq=true, mass density to use - BoutReal density_floor; // Minimum mass density if boussinesq=false - bool advection; // Advect fluids by an approximate polarisation velocity? - bool diamagnetic_polarisation; // Calculate compression terms? + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/quasineutral.hxx b/include/quasineutral.hxx index 63340faf0..f48cfb2f5 100644 --- a/include/quasineutral.hxx +++ b/include/quasineutral.hxx @@ -23,28 +23,35 @@ struct Quasineutral : public Component { /// - charge Required to have a particle charge /// - AA Atomic mass /// - Quasineutral(std::string name, Options &alloptions, Solver *UNUSED(solver)); - - /// - /// Sets in state - /// - species - /// - - /// - density - /// - charge - /// - AA - void transform(Options &state) override; + Quasineutral(std::string name, Options& alloptions, Solver* UNUSED(solver)); /// Get the final density for output /// including any boundary conditions applied - void finally(const Options &state) override; + void finally(const Options& state) override; + + void outputVars(Options& state) override; - void outputVars(Options &state) override; private: std::string name; ///< Name of this species BoutReal charge; ///< The charge of this species BoutReal AA; ///< Atomic mass - Field3D density; ///< The density (for writing to output) + Field3D density; ///< The density (for writing to output) + + /// + /// Reads in state + /// - species + /// - + /// - charge [if density and charge are set] + /// - density [if density and charge are set] + /// + /// Sets in state + /// - species + /// - + /// - density + /// - charge + /// - AA + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/rate_helper.hxx b/include/rate_helper.hxx index a2ff8720e..6da39348d 100644 --- a/include/rate_helper.hxx +++ b/include/rate_helper.hxx @@ -26,12 +26,12 @@ struct RateHelper { * factor, n_e and T_e * @param region the region in which to calculate the rate */ - RateHelper(const Options& state, const std::vector& reactant_names, + RateHelper(const GuardedOptions state, const std::vector& reactant_names, RateFunctionType rate_calc_func, const Region region) : region(region), rate_calc_func(rate_calc_func) { // Extract electron properties from state - const Options& electron = state["species"]["e"]; + const GuardedOptions electron = state["species"]["e"]; n_e = get(electron["density"]); T_e = get(electron["temperature"]); diff --git a/include/reaction.hxx b/include/reaction.hxx index 21e34681b..81e17ac5c 100644 --- a/include/reaction.hxx +++ b/include/reaction.hxx @@ -7,7 +7,7 @@ #include "reaction_diagnostic.hxx" #include "reaction_parser.hxx" -typedef Options& (*OPTYPE)(Options&, Field3D); +using OPTYPE = GuardedOptions &&(GuardedOptions&&, Field3D); /** * @brief Temporary struct to use as a base class for all reactions components. Ensures @@ -15,7 +15,8 @@ typedef Options& (*OPTYPE)(Options&, Field3D); * all reaction classes have been refactored to inherit from Reaction. */ struct ReactionBase : public Component { - ReactionBase() : inst_num(get_instance_num() + 1) {} + ReactionBase(Permissions&& permissions) + : Component(std::move(permissions)), inst_num(get_instance_num() + 1) {} static int get_instance_num() { static int instance_num{0}; return instance_num++; @@ -30,9 +31,7 @@ protected: * */ struct Reaction : public ReactionBase { - Reaction(std::string name, Options& alloptions); - - void transform(Options& state) override final; + Reaction(std::string name, Options& alloptionss, const std::string & reaction_str); void outputVars(Options& state) override final; @@ -85,7 +84,7 @@ protected: * * @param state Current sim state */ - void calc_weightsums(Options& state); + void calc_weightsums(GuardedOptions & state); /** * @brief Evaluate at a particular density and temperature @@ -125,7 +124,7 @@ protected: * @param state * @param reaction_rate */ - virtual void transform_additional([[maybe_unused]] Options& state, + virtual void transform_additional([[maybe_unused]] GuardedOptions& state, [[maybe_unused]] Field3D& reaction_rate) {} /** @@ -140,7 +139,7 @@ protected: * @param fld the field used in the update */ template - void update_source(Options& state, const std::string& sp_name, + void update_source(GuardedOptions& state, const std::string& sp_name, ReactionDiagnosticType type, Field3D& fld) { // Update species data operation(state["species"][sp_name][state_labels.at(type)], fld); @@ -171,6 +170,8 @@ private: /// Participation factors of all species std::map pfactors; - void zero_diagnostics(Options& state); + void zero_diagnostics(GuardedOptions& state); + + void transform_impl(GuardedOptions& state) override final; }; #endif diff --git a/include/recycling.hxx b/include/recycling.hxx index 995effcf5..af73b03d7 100644 --- a/include/recycling.hxx +++ b/include/recycling.hxx @@ -22,20 +22,6 @@ struct Recycling : public Component { /// Recycling(std::string name, Options &alloptions, Solver *); - /// Inputs - /// - /// - species - /// - - /// - density - /// - velocity - /// - /// Outputs - /// - /// - species - /// - - /// - density_source - /// - void transform(Options &state) override; void outputVars(Options &state) override; private: @@ -79,6 +65,27 @@ private: Field3D particle_flow_xlow; ///< Radial wall particle fluxes for recycling calc. No need to get poloidal from here, it's calculated from sheath velocity Field2D is_pump; ///< 1 = pump, 0 = no pump. Works only in SOL/PFR. Provided by user in grid file. + /// Inputs + /// + /// - species + /// - + /// - density + /// - velocity + /// - temperature + /// - + /// - AA + /// - density + /// - pressure + /// - temperature + /// + /// Outputs + /// + /// - species + /// - + /// - density_source + /// - energy_source + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/relax_potential.hxx b/include/relax_potential.hxx index bb6668d54..c24afbfaa 100644 --- a/include/relax_potential.hxx +++ b/include/relax_potential.hxx @@ -27,26 +27,6 @@ struct RelaxPotential : public Component { /// RelaxPotential(std::string name, Options& options, Solver* solver); - /// Optional inputs - /// - /// - species - /// - pressure and charge => Calculates diamagnetic terms [if diamagnetic=true] - /// - pressure, charge and mass => Calculates polarisation current terms - /// [if diamagnetic_polarisation=true] - /// - /// Sets in the state - /// - species - /// - [if has pressure and charge] - /// - energy_source - /// - fields - /// - vorticity - /// - phi Electrostatic potential - /// - DivJdia Divergence of diamagnetic current [if diamagnetic=true] - /// - /// Note: Diamagnetic current calculated here, but could be moved - /// to a component with the diamagnetic drift advection terms - void transform(Options& state) override; - /// Optional inputs /// - fields /// - DivJextra Divergence of current, including parallel current @@ -76,6 +56,26 @@ private: Vector2D Curlb_B; ///< Curvature vector Curl(b/B) BoutReal lambda_1, lambda_2; ///< Relaxation parameters + + /// Optional inputs + /// + /// - species + /// - pressure and charge => Calculates diamagnetic terms [if diamagnetic=true] + /// - pressure, charge and mass => Calculates polarisation current terms + /// [if diamagnetic_polarisation=true] + /// + /// Sets in the state + /// - species + /// - [if has pressure and charge] + /// - energy_source + /// - fields + /// - vorticity + /// - phi Electrostatic potential + /// - DivJdia Divergence of diamagnetic current [if diamagnetic=true] + /// + /// Note: Diamagnetic current calculated here, but could be moved + /// to a component with the diamagnetic drift advection terms + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/scale_timederivs.hxx b/include/scale_timederivs.hxx index 4471188d6..8476e3654 100644 --- a/include/scale_timederivs.hxx +++ b/include/scale_timederivs.hxx @@ -11,38 +11,45 @@ /// where the aim is to reach ddt -> 0 /// struct ScaleTimeDerivs : public Component { - ScaleTimeDerivs(std::string, Options&, Solver*) {} + ScaleTimeDerivs(std::string, Options&, Solver*) + : Component({readOnly("species:e:temperature"), writeFinal("scale_timederivs")}) {} + void outputVars(Options& state) override { + set_with_attrs( + state["scale_timederivs"], scaling, + {{"time_dimension", "t"}, + {"long_name", "Scaling factor applied to all time derivatives"}, + {"source", "scale_timederivs"}}); + } +private: + Field3D scaling; // The scaling factor applied to each cell + + /// Inputs + /// + /// - species + /// - e + /// - temperature + /// /// Sets in the state /// /// - scale_timederivs /// - void transform(Options &state) override { + void transform_impl(GuardedOptions& state) override { auto* coord = bout::globals::mesh->getCoordinates(); Field2D dl2 = coord->g_22 * SQ(coord->dy); // Scale by parallel heat conduction CFL timescale auto Te = get(state["species"]["e"]["temperature"]); - Field3D dt = dl2 / pow(floor(Te, 1e-5), 5./2); + Field3D dt = dl2 / pow(floor(Te, 1e-5), 5. / 2); scaling = dt / max(dt, true); // Saved for output - state["scale_timederivs"] = scaling; + state["scale_timederivs"].getWritable() = scaling; } - - void outputVars(Options& state) override { - set_with_attrs( - state["scale_timederivs"], scaling, - {{"time_dimension", "t"}, - {"long_name", "Scaling factor applied to all time derivatives"}, - {"source", "scale_timederivs"}}); - } -private: - Field3D scaling; // The scaling factor applied to each cell }; namespace { -RegisterComponent registercomponentscaletimederivs("scale_timederivs"); + RegisterComponent registercomponentscaletimederivs("scale_timederivs"); } #endif // SCALE_TIMEDERIVS_H diff --git a/include/set_temperature.hxx b/include/set_temperature.hxx index 2bb51c7e1..09d433fc8 100644 --- a/include/set_temperature.hxx +++ b/include/set_temperature.hxx @@ -23,7 +23,12 @@ struct SetTemperature : public Component { /// - /// - temperature_from name of species SetTemperature(std::string name, Options& alloptions, Solver* UNUSED(solver)) - : name(name) { + : Component({readIfSet("species:{name}:density", Regions::Interior), + readOnly("species:{from}:temperature"), + readWrite("species:{name}:temperature"), + // FIXME: Only written if density set + readWrite("species:{name}:pressure")}), + name(name) { AUTO_TRACE(); auto& options = alloptions[name]; @@ -35,13 +40,42 @@ struct SetTemperature : public Component { diagnose = options["diagnose"] .doc("Save additional output diagnostics") .withDefault(false); + + substitutePermissions("name", {name}); + substitutePermissions("from", {temperature_from}); } + void outputVars(Options& state) override { + AUTO_TRACE(); + + if (diagnose) { + auto Tnorm = get(state["Tnorm"]); + + // Save temperature to output files + set_with_attrs(state[std::string("T") + name], T, + {{"time_dimension", "t"}, + {"units", "eV"}, + {"conversion", Tnorm}, + {"standard_name", "temperature"}, + {"long_name", name + " temperature set from " + temperature_from}, + {"species", name}, + {"source", "set_temperature"}}); + } + } + +private: + std::string name; ///< Short name of species e.g "e" + std::string temperature_from; ///< The species that the temperature is taken from + Field3D T; ///< The temperature + bool diagnose; ///< Output diagnostics? + /// /// Inputs /// - species /// - /// - temperature + /// - + /// - density (if set) /// /// Sets in the state: /// - species @@ -49,14 +83,14 @@ struct SetTemperature : public Component { /// - temperature /// - pressure (if density is set) /// - void transform(Options& state) override { + void transform_impl(GuardedOptions& state) override { AUTO_TRACE(); // Get the temperature T = GET_NOBOUNDARY(Field3D, state["species"][temperature_from]["temperature"]); // Set temperature - auto& species = state["species"][name]; + auto species = state["species"][name]; set(species["temperature"], T); if (isSetFinalNoBoundary(species["density"])) { @@ -65,30 +99,6 @@ struct SetTemperature : public Component { set(species["pressure"], N * T); } } - - void outputVars(Options& state) override { - AUTO_TRACE(); - - if (diagnose) { - auto Tnorm = get(state["Tnorm"]); - - // Save temperature to output files - set_with_attrs(state[std::string("T") + name], T, - {{"time_dimension", "t"}, - {"units", "eV"}, - {"conversion", Tnorm}, - {"standard_name", "temperature"}, - {"long_name", name + " temperature set from " + temperature_from}, - {"species", name}, - {"source", "set_temperature"}}); - } - } - -private: - std::string name; ///< Short name of species e.g "e" - std::string temperature_from; ///< The species that the temperature is taken from - Field3D T; ///< The temperature - bool diagnose; ///< Output diagnostics? }; namespace { diff --git a/include/sheath_boundary.hxx b/include/sheath_boundary.hxx index c3e2532f5..0cd5f83a9 100644 --- a/include/sheath_boundary.hxx +++ b/include/sheath_boundary.hxx @@ -31,6 +31,19 @@ struct SheathBoundary : public Component { /// - always_set_phi Always set phi field? Default is to only modify if already set SheathBoundary(std::string name, Options &options, Solver *); +private: + BoutReal Ge; // Secondary electron emission coefficient + Field3D sin_alpha; // sin of angle between magnetic field and wall. + + bool lower_y; // Boundary on lower y? + bool upper_y; // Boundary on upper y? + + bool always_set_phi; ///< Set phi field? + + Field3D wall_potential; ///< Voltage at the wall. Normalised units. + + bool floor_potential; ///< Apply floor to sheath potential? + /// /// # Inputs /// - species @@ -39,11 +52,11 @@ struct SheathBoundary : public Component { /// - temperature /// - pressure Optional /// - velocity Optional - /// - mass Optional + /// - AA Optional /// - adiabatic Optional. Ratio of specific heats, default 5/3. /// - if charge is set (i.e. not neutrals) /// - charge - /// - mass + /// - AA /// - density /// - temperature /// - pressure Optional @@ -58,11 +71,13 @@ struct SheathBoundary : public Component { /// - e /// - density Sets boundary /// - temperature Sets boundary + /// - pressure Sets boundary /// - velocity Sets boundary /// - energy_source /// - /// - density Sets boundary /// - temperature Sets boundary + /// - pressure Sets boundary /// - velocity Sets boundary /// - momentum Sets boundary /// - energy_source @@ -74,19 +89,7 @@ struct SheathBoundary : public Component { /// Note that phi in the domain will not be set, so will be invalid data. /// /// - void transform(Options &state) override; -private: - BoutReal Ge; // Secondary electron emission coefficient - Field3D sin_alpha; // sin of angle between magnetic field and wall. - - bool lower_y; // Boundary on lower y? - bool upper_y; // Boundary on upper y? - - bool always_set_phi; ///< Set phi field? - - Field3D wall_potential; ///< Voltage at the wall. Normalised units. - - bool floor_potential; ///< Apply floor to sheath potential? + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/sheath_boundary_insulating.hxx b/include/sheath_boundary_insulating.hxx index ecc0fc966..a29afa24c 100644 --- a/include/sheath_boundary_insulating.hxx +++ b/include/sheath_boundary_insulating.hxx @@ -13,6 +13,15 @@ struct SheathBoundaryInsulating : public Component { SheathBoundaryInsulating(std::string name, Options &options, Solver *); +private: + BoutReal Ge; // Secondary electron emission coefficient + BoutReal sin_alpha; // sin of angle between magnetic field and wall. + + bool lower_y; // Boundary on lower y? + bool upper_y; // Boundary on upper y? + + BoutReal gamma_e; ///< Electron sheath heat transmission + /// /// Inputs /// - species @@ -56,15 +65,7 @@ struct SheathBoundaryInsulating : public Component { /// Note that phi in the domain will not be set, so will be invalid data. /// /// - void transform(Options &state) override; -private: - BoutReal Ge; // Secondary electron emission coefficient - BoutReal sin_alpha; // sin of angle between magnetic field and wall. - - bool lower_y; // Boundary on lower y? - bool upper_y; // Boundary on upper y? - - BoutReal gamma_e; ///< Electron sheath heat transmission + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/sheath_boundary_simple.hxx b/include/sheath_boundary_simple.hxx index 6f4cf975c..76ea18bce 100644 --- a/include/sheath_boundary_simple.hxx +++ b/include/sheath_boundary_simple.hxx @@ -29,6 +29,34 @@ struct SheathBoundarySimple : public Component { /// - always_set_phi Always set phi field? Default is to only modify if already set SheathBoundarySimple(std::string name, Options &options, Solver *); + void outputVars(Options &state) override; + +private: + BoutReal Ge; // Secondary electron emission coefficient + BoutReal sin_alpha; // sin of angle between magnetic field and wall. + + BoutReal gamma_e; ///< Electron sheath heat transmission + BoutReal gamma_i; ///< Ion sheath heat transmission + BoutReal sheath_ion_polytropic; ///< Polytropic coefficient in sheat velocity + + bool lower_y; // Boundary on lower y? + bool upper_y; // Boundary on upper y? + + bool always_set_phi; ///< Set phi field? + + Field3D wall_potential; ///< Voltage of the wall. Normalised units. + + Field3D hflux_e; // Electron heat flux through sheath + Field3D phi; // Phi at sheath + Field3D ion_sum; // Sum of ion current at sheath + + bool diagnose; // Save diagnostic variables? + Options diagnostics; // Options object to store diagnostic fields like a dict + + bool no_flow; ///< No flow speed, only remove energy + + BoutReal density_boundary_mode, pressure_boundary_mode, temperature_boundary_mode; ///< BC mode: 0=LimitFree, 1=ExponentialFree, 2=LinearFree + /// /// # Inputs /// - species @@ -38,7 +66,6 @@ struct SheathBoundarySimple : public Component { /// - pressure Optional /// - velocity Optional /// - mass Optional - /// - adiabatic Optional. Ratio of specific heats, default 5/3. /// - if charge is set (i.e. not neutrals) /// - charge /// - mass @@ -47,7 +74,6 @@ struct SheathBoundarySimple : public Component { /// - pressure Optional /// - velocity Optional. Default 0 /// - momentum Optional. Default mass * density * velocity - /// - adiabatic Optional. Ratio of specific heats, default 5/3. /// - fields /// - phi Optional. If not set, calculated at boundary (see note below) /// @@ -72,33 +98,7 @@ struct SheathBoundarySimple : public Component { /// Note that phi in the domain will not be set, so will be invalid data. /// /// - void transform(Options &state) override; - void outputVars(Options &state) override; -private: - BoutReal Ge; // Secondary electron emission coefficient - BoutReal sin_alpha; // sin of angle between magnetic field and wall. - - BoutReal gamma_e; ///< Electron sheath heat transmission - BoutReal gamma_i; ///< Ion sheath heat transmission - BoutReal sheath_ion_polytropic; ///< Polytropic coefficient in sheat velocity - - bool lower_y; // Boundary on lower y? - bool upper_y; // Boundary on upper y? - - bool always_set_phi; ///< Set phi field? - - Field3D wall_potential; ///< Voltage of the wall. Normalised units. - - Field3D hflux_e; // Electron heat flux through sheath - Field3D phi; // Phi at sheath - Field3D ion_sum; // Sum of ion current at sheath - - bool diagnose; // Save diagnostic variables? - Options diagnostics; // Options object to store diagnostic fields like a dict - - bool no_flow; ///< No flow speed, only remove energy - - BoutReal density_boundary_mode, pressure_boundary_mode, temperature_boundary_mode; ///< BC mode: 0=LimitFree, 1=ExponentialFree, 2=LinearFree + void transform_impl(GuardedOptions& state) override; }; namespace { @@ -106,4 +106,4 @@ RegisterComponent registercomponentsheathboundarysimple("sheath_boundary_simple"); } -#endif // SHEATH_BOUNDARY_SIMPLE_H \ No newline at end of file +#endif // SHEATH_BOUNDARY_SIMPLE_H diff --git a/include/sheath_closure.hxx b/include/sheath_closure.hxx index 07e60b4c0..bbd0569d0 100644 --- a/include/sheath_closure.hxx +++ b/include/sheath_closure.hxx @@ -17,34 +17,37 @@ struct SheathClosure : public Component { /// SheathClosure(std::string name, Options &options, Solver *); +private: + BoutReal L_par; // Normalised connection length + + BoutReal sheath_gamma; // Sheath heat transmission coefficient + + BoutReal sheath_gamma_ions; // Sheath heat transmission coefficient for ions + + BoutReal offset; // Potential at which the sheath current is zero + + bool sinks; // Include sinks of density and energy? + /// Inputs /// - fields /// - phi Electrostatic potential /// /// Optional inputs /// - species + /// - AA /// - density - /// - pressure + /// - temperature /// /// Modifies /// - species /// - e - /// - density_source (If density present) + /// - density_source + /// - energy_source (if temperature present) /// - density_source and energy_source (If sinks=true) /// - fields /// - DivJdia Divergence of current /// - void transform(Options &state) override; -private: - BoutReal L_par; // Normalised connection length - - BoutReal sheath_gamma; // Sheath heat transmission coefficient - - BoutReal sheath_gamma_ions; // Sheath heat transmission coefficient for ions - - BoutReal offset; // Potential at which the sheath current is zero - - bool sinks; // Include sinks of density and energy? + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/simple_conduction.hxx b/include/simple_conduction.hxx index 238ad0704..4eade14bb 100644 --- a/include/simple_conduction.hxx +++ b/include/simple_conduction.hxx @@ -14,7 +14,11 @@ /// Expressions taken from: /// https://farside.ph.utexas.edu/teaching/plasma/lectures1/node35.html struct SimpleConduction : public Component { - SimpleConduction(std::string name, Options& alloptions, Solver*) : name(name) { + SimpleConduction(std::string name, Options& alloptions, Solver*) + : Component({readOnly("species:{name}:temperature", Regions::Interior), + readOnly("species:{name}:AA"), + readWrite("species:{name}:energy_source")}), + name(name) { auto& units = alloptions["units"]; Tnorm = units["eV"]; Nnorm = units["inv_meters_cubed"]; @@ -54,10 +58,25 @@ struct SimpleConduction : public Component { boundary_flux = options["conduction_boundary_flux"] .doc("Allow heat conduction through sheath boundaries?") .withDefault(false); + + if (density <= 0.0) { + setPermissions(readOnly("species:{name}:density", Regions::Interior)); + } + substitutePermissions("name", {name}); } - void transform(Options& state) override { - auto& species = state["species"][name]; +private: + std::string name; ///< Name of the species e.g. "e" + BoutReal kappa0; ///< Pre-calculated constant in heat conduction coefficient + BoutReal Nnorm, Tnorm; ///< Normalisation coefficients + + BoutReal temperature; ///< Fix temperature if > 0 + BoutReal density; ///< Fix density if > 0 + + bool boundary_flux; ///< Allow flux through sheath boundaries? + + void transform_impl(GuardedOptions& state) override { + auto species = state["species"][name]; // Species time-evolving temperature Field3D T = GET_NOBOUNDARY(Field3D, species["temperature"]); @@ -85,16 +104,6 @@ struct SimpleConduction : public Component { add(species["energy_source"], DivQ); } - -private: - std::string name; ///< Name of the species e.g. "e" - BoutReal kappa0; ///< Pre-calculated constant in heat conduction coefficient - BoutReal Nnorm, Tnorm; ///< Normalisation coefficients - - BoutReal temperature; ///< Fix temperature if > 0 - BoutReal density; ///< Fix density if > 0 - - bool boundary_flux; ///< Allow flux through sheath boundaries? }; namespace { diff --git a/include/simple_pump.hxx b/include/simple_pump.hxx index dd0f85457..9a18b3b6a 100644 --- a/include/simple_pump.hxx +++ b/include/simple_pump.hxx @@ -9,7 +9,10 @@ struct SimplePump : public Component { - SimplePump(std::string name, Options& alloptions, Solver*) : name(name) { + SimplePump(std::string name, Options& alloptions, Solver*) + : Component({readOnly("species:{name}:density", Regions::Interior), + readWrite("species:{name}:density_source")}), + name(name) { Options& options = alloptions[name]; @@ -31,17 +34,8 @@ struct SimplePump : public Component { .doc("Output additional diagnostics?") .withDefault(false); - }; - - void transform(Options& state) override { - - Field3D species_density = getNoBoundary(state["species"][name]["density"]); - - pumping_sink = (sink_shape * species_density) * (-1.0 / residence_time); - - add(state["species"][name]["density_source"], pumping_sink); - - }; + substitutePermissions("name", {name}); + }; void outputVars(Options& state) override { AUTO_TRACE(); @@ -69,6 +63,15 @@ struct SimplePump : public Component { BoutReal residence_time; bool diagnose; + void transform_impl(GuardedOptions& state) override { + + Field3D species_density = getNoBoundary(state["species"][name]["density"]); + + pumping_sink = (sink_shape * species_density) * (-1.0 / residence_time); + + add(state["species"][name]["density_source"], pumping_sink); + + }; }; namespace { diff --git a/include/snb_conduction.hxx b/include/snb_conduction.hxx index bc609ce10..0d8868757 100644 --- a/include/snb_conduction.hxx +++ b/include/snb_conduction.hxx @@ -46,34 +46,44 @@ struct SNBConduction : public Component { /// Inputs /// - /// - diagnose Saves Div_Q_SH and Div_Q_SNB - SNBConduction(std::string name, Options& alloptions, Solver*) : snb(alloptions[name]) { + SNBConduction(std::string name, Options& alloptions, Solver*) + : Component({readOnly("species:e:density"), readOnly("species:e:temperature"), + readWrite("species:e:energy_source")}), + snb(alloptions[name]) { AUTO_TRACE(); auto& options = alloptions[name]; + auto& units = alloptions["units"]; + rho_s0 = get(units["meters"]); + Tnorm = get(units["eV"]); + Nnorm = get(units["inv_meters_cubed"]); + Omega_ci = 1. / get(units["seconds"]); + diagnose = options["diagnose"] .doc("Save additional output diagnostics") .withDefault(false); } + void outputVars(Options& state) override; +private: + bout::HeatFluxSNB snb; + + BoutReal rho_s0, Tnorm, Nnorm, Omega_ci; ///< Normalisations for units + Field3D Div_Q_SH, Div_Q_SNB; ///< Divergence of heat fluxes + + bool diagnose; ///< Output additional diagnostics? + /// Inputs /// - species /// - e /// - density - /// - collision_frequency + /// - temperature /// /// Sets /// - species /// - e /// - energy_source - void transform(Options& state) override; - - void outputVars(Options& state) override; -private: - bout::HeatFluxSNB snb; - - Field3D Div_Q_SH, Div_Q_SNB; ///< Divergence of heat fluxes - - bool diagnose; ///< Output additional diagnostics? + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/solkit_hydrogen_charge_exchange.hxx b/include/solkit_hydrogen_charge_exchange.hxx index 0a186dc9d..36eb95611 100644 --- a/include/solkit_hydrogen_charge_exchange.hxx +++ b/include/solkit_hydrogen_charge_exchange.hxx @@ -12,7 +12,10 @@ struct SOLKITHydrogenChargeExchange : public ReactionBase { /// - units /// - inv_meters_cubed /// - seconds - SOLKITHydrogenChargeExchange(std::string, Options& alloptions, Solver*) { + SOLKITHydrogenChargeExchange(std::string, Options& alloptions, Solver*) + : ReactionBase({readIfSet("species:{sp}:velocity"), + readOnly("species:{sp}:{readvals}"), + readWrite("species:{sp}:momentum_source")}) { // Get the units const auto& units = alloptions["units"]; Nnorm = get(units["inv_meters_cubed"]); @@ -31,7 +34,7 @@ struct SOLKITHydrogenChargeExchange : public ReactionBase { /// Sets in all species: /// - momentum_source /// - void calculate_rates(Options& atom, Options& ion); + void calculate_rates(GuardedOptions atom, GuardedOptions ion); protected: BoutReal Nnorm, rho_s0; ///< Normalisations @@ -43,10 +46,15 @@ protected: /// @tparam Isotope The isotope ('h', 'd' or 't') of the atom and ion template struct SOLKITHydrogenChargeExchangeIsotope : public SOLKITHydrogenChargeExchange { - SOLKITHydrogenChargeExchangeIsotope(std::string name, Options& alloptions, Solver* solver) - : SOLKITHydrogenChargeExchange(name, alloptions, solver) {} + SOLKITHydrogenChargeExchangeIsotope(std::string name, Options& alloptions, + Solver* solver) + : SOLKITHydrogenChargeExchange(name, alloptions, solver) { + substitutePermissions("sp", {{Isotope}, {Isotope, '+'}}); + substitutePermissions("readvals", {"AA", "density"}); + } - void transform(Options& state) override { +private: + void transform_impl(GuardedOptions& state) override { calculate_rates(state["species"][{Isotope}], // e.g. "h" state["species"][{Isotope, '+'}]); // e.g. "d+" } diff --git a/include/solkit_neutral_parallel_diffusion.hxx b/include/solkit_neutral_parallel_diffusion.hxx index 8f1242144..20ae154d4 100644 --- a/include/solkit_neutral_parallel_diffusion.hxx +++ b/include/solkit_neutral_parallel_diffusion.hxx @@ -18,8 +18,11 @@ struct SOLKITNeutralParallelDiffusion : public Component { /// - inv_meters_cubed /// - /// - neutral_temperature [eV] - /// - SOLKITNeutralParallelDiffusion(std::string name, Options &alloptions, Solver *) { + /// + SOLKITNeutralParallelDiffusion(std::string name, Options& alloptions, Solver*) + : Component({readOnly("species:{all_species}:{inputs}"), + readOnly("species:{neutrals}:AA"), + readWrite("species:{neutrals}:density_source")}) { auto Tnorm = get(alloptions["units"]["eV"]); auto& options = alloptions[name]; neutral_temperature = options["neutral_temperature"] @@ -30,24 +33,27 @@ struct SOLKITNeutralParallelDiffusion : public Component { auto Nnorm = get(alloptions["units"]["inv_meters_cubed"]); auto rho_s0 = get(alloptions["units"]["meters"]); area_norm = 1. / (Nnorm * rho_s0); + + substitutePermissions("inputs", {"charge", "density"}); } +private: + BoutReal neutral_temperature; ///< Fixed neutral t + BoutReal area_norm; ///< Area normalisation [m^2] + /// /// Inputs /// - species - /// - # Applies to all neutral species - /// - AA + /// - + /// - AA [neutral species only] + /// - charge /// - density /// /// Sets /// - species - /// - + /// - # Applies to all neutral species /// - density_source - void transform(Options &state) override; - -private: - BoutReal neutral_temperature; ///< Fixed neutral t - BoutReal area_norm; ///< Area normalisation [m^2] + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/sound_speed.hxx b/include/sound_speed.hxx index 69be20c35..8cf281476 100644 --- a/include/sound_speed.hxx +++ b/include/sound_speed.hxx @@ -11,7 +11,12 @@ /// This uses the sum of all species pressures and mass densities /// so should run after those have been set. struct SoundSpeed : public Component { - SoundSpeed(std::string name, Options &alloptions, Solver*) { + SoundSpeed(std::string name, Options& alloptions, Solver*) + : Component({readOnly("species:{all_species}:pressure", Regions::Interior), + writeFinal("sound_speed"), writeFinal("fastest_wave"), + readIfSet("species:{sp}:AA"), + // FIXME: Only read if AA is set + readIfSet("species:{sp}:{opt_inputs}", Regions::Interior)}) { Options &options = alloptions[name]; electron_dynamics = options["electron_dynamics"] .doc("Include electron sound speed?") @@ -40,10 +45,22 @@ struct SoundSpeed : public Component { if (temperature_floor > 0.0) { temperature_floor /= get(alloptions["units"]["eV"]); } + + substitutePermissions("sp", + {electron_dynamics ? "{all_species}" : "{non_electrons}"}); + substitutePermissions("opt_inputs", {"density", "temperature"}); } - + +private: + bool electron_dynamics; ///< Include electron sound speed? + bool alfven_wave; ///< Include Alfven wave speed? + BoutReal beta_norm{0.0}; ///< Normalisation factor for Alfven speed + BoutReal temperature_floor; ///< Minimum temperature when calculating speed + BoutReal fastest_wave_factor; ///< Multiply the fastest wave by this factor + /// This sets in the state - /// - sound_speed The collective sound speed, based on total pressure and total mass density + /// - sound_speed The collective sound speed, based on total pressure and total mass + /// density /// - fastest_wave The highest species sound speed at each point in the domain /// /// Optional inputs: @@ -52,15 +69,9 @@ struct SoundSpeed : public Component { /// - density /// - AA // Atomic mass /// - pressure + /// - temperature /// - void transform(Options &state) override; - -private: - bool electron_dynamics; ///< Include electron sound speed? - bool alfven_wave; ///< Include Alfven wave speed? - BoutReal beta_norm{0.0}; ///< Normalisation factor for Alfven speed - BoutReal temperature_floor; ///< Minimum temperature when calculating speed - BoutReal fastest_wave_factor; ///< Multiply the fastest wave by this factor + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/temperature_feedback.hxx b/include/temperature_feedback.hxx index b4975aed2..f36459288 100644 --- a/include/temperature_feedback.hxx +++ b/include/temperature_feedback.hxx @@ -22,7 +22,10 @@ struct TemperatureFeedback : public Component { /// - T (e.g. "Td+") /// - source_shape The initial source that is scaled by a time-varying factor /// - TemperatureFeedback(std::string name, Options& alloptions, Solver*) : name(name) { + TemperatureFeedback(std::string name, Options& alloptions, Solver*) + : Component({readOnly("species:{name}:temperature", Regions::Interior), + readOnly("time"), readWrite("species:{sp}:energy_source")}), + name(name) { Options& options = alloptions[name]; const auto& units = alloptions["units"]; @@ -82,18 +85,13 @@ struct TemperatureFeedback : public Component { diagnose = options["diagnose"] .doc("Output additional diagnostics?") .withDefault(false); - } - /// Inputs - /// - - /// - temperature - /// - /// Outputs - /// - /// - - /// - temperature_source - /// - void transform(Options& state) override; + std::vector species_stripped; + std::transform(species_list.begin(), species_list.end(), species_stripped.begin(), + [](const std::string& val) { return trim(val); }); + substitutePermissions("name", {name}); + substitutePermissions("sp", species_stripped); + } void outputVars(Options& state) override { AUTO_TRACE(); @@ -191,6 +189,17 @@ private: BoutReal proportional_term, integral_term; ///< Components of resulting source for diagnostics bool diagnose; ///< Output diagnostic information? + + /// Inputs + /// - + /// - temperature + /// + /// Outputs + /// + /// - + /// - temperature_source + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/transform.hxx b/include/transform.hxx index 8387196ab..ff302f864 100644 --- a/include/transform.hxx +++ b/include/transform.hxx @@ -9,10 +9,10 @@ struct Transform : public Component { Transform(std::string name, Options& options, Solver*); - void transform(Options& state) override; - private: std::map transforms; + + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/upstream_density_feedback.hxx b/include/upstream_density_feedback.hxx index 2a3a11fce..0975e1219 100644 --- a/include/upstream_density_feedback.hxx +++ b/include/upstream_density_feedback.hxx @@ -20,7 +20,16 @@ struct UpstreamDensityFeedback : public Component { /// - N (e.g. "Nd+") /// - source_shape The initial source that is scaled by a time-varying factor /// - UpstreamDensityFeedback(std::string name, Options& alloptions, Solver*) : name(name) { + UpstreamDensityFeedback(std::string name, Options& alloptions, Solver*) + : Component({readOnly("time"), + readOnly("species:{name}:density", Regions::Interior), + // FIXME: These are only read if BOTH are set + readIfSet("species:{name}:AA"), + readIfSet("species:{name}:velocity", Regions::Interior), + readWrite("species:{name}:density_source"), + // FIXME: This is only set if AA and density_source are set + readWrite("species:{name}:energy_source")}), + name(name) { const auto& units = alloptions["units"]; BoutReal Nnorm = get(units["inv_meters_cubed"]); BoutReal FreqNorm = 1. / get(units["seconds"]); @@ -59,18 +68,9 @@ struct UpstreamDensityFeedback : public Component { diagnose = options["diagnose"] .doc("Output additional diagnostics?") .withDefault(false); - } - /// Inputs - /// - - /// - density - /// - /// Outputs - /// - /// - - /// - density_source - /// - void transform(Options& state) override; + substitutePermissions("name", {name}); + } void outputVars(Options& state) override { AUTO_TRACE(); @@ -161,6 +161,20 @@ private: BoutReal proportional_term, integral_term; ///< Components of resulting source for diagnostics bool diagnose; ///< Output diagnostic information? + + /// Inputs + /// - + /// - density + /// - velocity (if set) + /// - AA (if set) + /// + /// Outputs + /// + /// - + /// - density_source + /// - energy_source (if velocity and AA are set) + /// + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/vorticity.hxx b/include/vorticity.hxx index 42218caab..70d0243e7 100644 --- a/include/vorticity.hxx +++ b/include/vorticity.hxx @@ -57,25 +57,6 @@ struct Vorticity : public Component { /// Vorticity(std::string name, Options &options, Solver *solver); - /// Optional inputs - /// - /// - species - /// - pressure and charge => Calculates diamagnetic terms [if diamagnetic=true] - /// - pressure, charge and mass => Calculates polarisation current terms [if diamagnetic_polarisation=true] - /// - /// Sets in the state - /// - species - /// - [if has pressure and charge] - /// - energy_source - /// - fields - /// - vorticity - /// - phi Electrostatic potential - /// - DivJdia Divergence of diamagnetic current [if diamagnetic=true] - /// - /// Note: Diamagnetic current calculated here, but could be moved - /// to a component with the diamagnetic drift advection terms - void transform(Options &state) override; - /// Optional inputs /// - fields /// - DivJextra Divergence of current, including parallel current @@ -145,6 +126,29 @@ private: Field3D DivJdia, DivJcol; // Divergence of diamagnetic and collisional current bool diagnose; ///< Output additional diagnostics? + + /// Optional inputs + /// + /// - species + /// - pressure and charge => Calculates diamagnetic terms [if diamagnetic=true] + /// - pressure, charge and mass => Calculates polarisation current terms [if + /// diamagnetic_polarisation=true] + /// - density, charge, and collision_frequency => Calculate damping due to friction + /// [if collisional_friction=true] + /// + /// Sets in the state + /// - species + /// - [if has pressure and charge and diamagnetic=true] + /// - energy_source + /// - fields + /// - vorticity + /// - phi Electrostatic potential + /// - DivJdia Divergence of diamagnetic current [if diamagnetic=true] + /// - DivJcol [if collisional_friction=true] + /// + /// Note: Diamagnetic current calculated here, but could be moved + /// to a component with the diamagnetic drift advection terms + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/include/zero_current.hxx b/include/zero_current.hxx index 27a55f9cf..5a00eb758 100644 --- a/include/zero_current.hxx +++ b/include/zero_current.hxx @@ -19,6 +19,19 @@ struct ZeroCurrent : public Component { /// - /// - charge (must not be zero) ZeroCurrent(std::string name, Options& alloptions, Solver*); + + void finally(const Options &state) override { + // Get the velocity with boundary condition applied. + // This is for output only + velocity = get(state["species"][name]["velocity"]); + } + + void outputVars(Options &state) override; +private: + std::string name; ///< Name of this species + BoutReal charge; ///< The charge of this species + + Field3D velocity; ///< Species velocity (for writing to output) /// Required inputs /// - species @@ -35,20 +48,7 @@ struct ZeroCurrent : public Component { /// - /// - velocity /// - void transform(Options &state) override; - - void finally(const Options &state) override { - // Get the velocity with boundary condition applied. - // This is for output only - velocity = get(state["species"][name]["velocity"]); - } - - void outputVars(Options &state) override; -private: - std::string name; ///< Name of this species - BoutReal charge; ///< The charge of this species - - Field3D velocity; ///< Species velocity (for writing to output) + void transform_impl(GuardedOptions& state) override; }; namespace { diff --git a/src/adas_reaction.cxx b/src/adas_reaction.cxx index 1af741dac..741bdf782 100644 --- a/src/adas_reaction.cxx +++ b/src/adas_reaction.cxx @@ -112,7 +112,7 @@ BoutReal OpenADASRateCoefficient::evaluate(BoutReal T, BoutReal n) { return pow(10., eval_log_coef); } -void OpenADAS::calculate_rates(Options& electron, Options& from_ion, Options& to_ion) { +void OpenADAS::calculate_rates(GuardedOptions && electron, GuardedOptions && from_ion, GuardedOptions && to_ion) { AUTO_TRACE(); Field3D Ne = GET_VALUE(Field3D, electron["density"]); @@ -173,9 +173,9 @@ void OpenADAS::calculate_rates(Options& electron, Options& from_ion, Options& to subtract(electron["energy_source"], energy_loss); } -void OpenADASChargeExchange::calculate_rates(Options& electron, Options& from_A, - Options& from_B, Options& to_A, - Options& to_B) { +void OpenADASChargeExchange::calculate_rates(GuardedOptions && electron, GuardedOptions && from_A, + GuardedOptions && from_B, GuardedOptions && to_A, + GuardedOptions && to_B) { AUTO_TRACE(); // Check that the reaction conserves mass and charge diff --git a/src/amjuel_reaction.cxx b/src/amjuel_reaction.cxx index c3cd94d83..f0fe74b98 100644 --- a/src/amjuel_reaction.cxx +++ b/src/amjuel_reaction.cxx @@ -57,7 +57,7 @@ BoutReal AmjuelReaction::eval_sigma_v(BoutReal T, BoutReal n) { return eval_amjuel_fit(T, n, amjuel_data.sigma_v_coeffs); } -void AmjuelReaction::transform_additional(Options& state, Field3D& reaction_rate) { +void AmjuelReaction::transform_additional(GuardedOptions& state, Field3D& reaction_rate) { // Amjuel-based reactions are assumed to have exactly 2 reactants, for now. std::vector reactant_species = @@ -69,7 +69,7 @@ void AmjuelReaction::transform_additional(Options& state, Field3D& reaction_rate parser->get_species(reactant_species, species_filter::heavy); // Amjuel-based reactions are assumed to have exactly 1 heavy reactant, for now. ASSERT1(heavy_reactant_species.size() == 1); - Options& rh = state["species"][heavy_reactant_species[0]]; + GuardedOptions rh = state["species"][heavy_reactant_species[0]]; BoutReal AA_rh = get(rh["AA"]); Field3D n_rh = get(rh["density"]); Field3D v_rh = get(rh["velocity"]); @@ -79,7 +79,7 @@ void AmjuelReaction::transform_additional(Options& state, Field3D& reaction_rate parser->get_species(species_filter::heavy, species_filter::products); // Amjuel-based reactions are assumed to have exactly 1 heavy product, for now. ASSERT1(heavy_product_species.size() == 1); - Options& ph = state["species"][heavy_product_species[0]]; + GuardedOptions ph = state["species"][heavy_product_species[0]]; Field3D v_ph = get(ph["velocity"]); // Kinetic energy transfer to thermal energy @@ -116,11 +116,11 @@ void AmjuelReaction::transform_additional(Options& state, Field3D& reaction_rate add(ph["energy_source"], 0.5 * AA_rh * reaction_rate * SQ(v_rh - v_ph)); // Energy source for electrons due to pop change - Options& electron = state["species"]["e"]; + GuardedOptions electron = state["species"]["e"]; Field3D T_e = get(electron["temperature"]); const int e_pop_change = this->parser->get_stoich().at("e"); if (e_pop_change != 0) { - if (electron.isSet("velocity")) { + if (IS_SET(electron["velocity"])) { // Transfer of electron kinetic to thermal energy due to density source // For ionisation: // Electrons with zero average velocity are created, diluting the kinetic energy. @@ -176,6 +176,6 @@ void AmjuelReaction::transform_additional(Options& state, Field3D& reaction_rate std::vector neutral_species = parser->get_species(species_filter::neutral); ASSERT1(neutral_species.size() == 1); set(state["species"][neutral_species[0]]["collision_frequencies"] - [rh.name() + "_" + ph.name() + "_" + this->short_reaction_type], + [heavy_reactant_species[0] + "_" + heavy_product_species[0] + "_" + this->short_reaction_type], heavy_particle_frequency); -} \ No newline at end of file +} diff --git a/src/anomalous_diffusion.cxx b/src/anomalous_diffusion.cxx index cc585efaa..45fac7041 100644 --- a/src/anomalous_diffusion.cxx +++ b/src/anomalous_diffusion.cxx @@ -7,7 +7,10 @@ using bout::globals::mesh; AnomalousDiffusion::AnomalousDiffusion(std::string name, Options& alloptions, Solver*) - : name(name) { + : Component({readOnly("species:{name}:density", Regions::Interior), + readIfSet("species:{name}:{optional}", Regions::Interior), + readWrite("species:{name}:{output}")}), + name(name) { // Normalisations const Options& units = alloptions["units"]; const BoutReal rho_s0 = units["meters"]; @@ -50,12 +53,33 @@ AnomalousDiffusion::AnomalousDiffusion(std::string name, Options& alloptions, So diagnose = alloptions[name]["diagnose"] .doc("Output additional diagnostics?") .withDefault(false); + + substitutePermissions("name", {name}); + substitutePermissions("optional", {"temperature", "velocity"}); + std::vector output_vars; + if (include_D) { + output_vars.push_back("density_source"); + output_vars.push_back("particle_flow_xlow"); + output_vars.push_back("particle_flow_ylow"); + } + if (include_D or include_chi) { + output_vars.push_back("energy_source"); + output_vars.push_back("energy_flow_xlow"); + output_vars.push_back("energy_flow_ylow"); + } + if (include_D or include_nu) { + setPermissions(readOnly(fmt::format("species:{}:AA", name))); + output_vars.push_back("momentum_source"); + output_vars.push_back("momentum_flow_xlow"); + output_vars.push_back("momentum_flow_ylow"); + } + substitutePermissions("output", output_vars); } -void AnomalousDiffusion::transform(Options& state) { +void AnomalousDiffusion::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& species = state["species"][name]; + GuardedOptions species = state["species"][name]; // Diffusion operates on 2D (axisymmetric) profiles // Note: Includes diffusion in Y, so set boundary fluxes @@ -69,7 +93,7 @@ void AnomalousDiffusion::transform(Options& state) { Field2D T2D = DC(T); const Field3D V = - species.isSet("velocity") ? GET_NOBOUNDARY(Field3D, species["velocity"]) : 0.0; + species.isSet("velocity") ? GET_NOBOUNDARY(Field3D, species["velocity"]) : 0.0; Field2D V2D = DC(V); if (!anomalous_sheath_flux) { @@ -168,4 +192,3 @@ void AnomalousDiffusion::outputVars(Options& state) { {"source", "anomalous_diffusion"}}); } } - diff --git a/src/binormal_stpm.cxx b/src/binormal_stpm.cxx index c855e776f..ce4c8ba95 100644 --- a/src/binormal_stpm.cxx +++ b/src/binormal_stpm.cxx @@ -9,7 +9,12 @@ using bout::globals::mesh; BinormalSTPM::BinormalSTPM(std::string name, Options& alloptions, [[maybe_unused]] Solver* solver) - : name(name) { + : Component({ + readIfSet("species:{all_species}:{input}", Regions::Interior), + readOnly("species:{all_species}:AA"), + readWrite("species:{all_species}:{output}"), + }), + name(name) { AUTO_TRACE(); auto& options = alloptions[name]; const Options& units = alloptions["units"]; @@ -45,16 +50,19 @@ BinormalSTPM::BinormalSTPM(std::string name, Options& alloptions, diagnose = options["diagnose"] .doc("Output diagnostics?") .withDefault(false); + + substitutePermissions("input", {"density", "temperature", "momentum"}); + substitutePermissions("output", {"energy_source", "momentum_source", "density_source"}); } -void BinormalSTPM::transform(Options& state) { +void BinormalSTPM::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; // Loop through all species for (auto& kv : allspecies.getChildren()) { const auto& species_name = kv.first; - Options& species = allspecies[species_name]; + GuardedOptions species = allspecies[species_name]; auto AA = get(species["AA"]); const Field3D N = species.isSet("density") diff --git a/src/braginskii_collisions.cxx b/src/braginskii_collisions.cxx index 0d0a952a2..8fca60f0d 100644 --- a/src/braginskii_collisions.cxx +++ b/src/braginskii_collisions.cxx @@ -20,8 +20,12 @@ #include "../include/component.hxx" #include "../include/hermes_utils.hxx" -BraginskiiCollisions::BraginskiiCollisions(const std::string& name, Options& alloptions, - Solver*) { +BraginskiiCollisions::BraginskiiCollisions(const std::string& name, Options& alloptions, Solver*) + : Component({readOnly("species:{all_species}:density", Regions::Interior), + readOnly("species:{electrons}:temperature", Regions::Interior), + readIfSet("species:{non_electrons}:charge"), + readIfSet("species:{negative_ions}:temperature", Regions::Interior), + readOnly("species:{all_species}:AA")}) { AUTO_TRACE(); const Options& units = alloptions["units"]; @@ -58,6 +62,54 @@ BraginskiiCollisions::BraginskiiCollisions(const std::string& name, Options& all diagnose = options["diagnose"].doc("Output additional diagnostics?").withDefault(false); + + if (electron_electron) { + setPermissions(readWrite( + "species:{electrons}:collision_frequencies:{electrons}_{electrons2}_coll")); + } + if (electron_ion) { + setPermissions(readOnly("species:{positive_ions}:temperature", Regions::Interior)); + setPermissions( + readWrite("species:{positive_ions}:collision_frequencies:{positive_ions}_{" + "electrons}_coll")); + setPermissions(readWrite( + "species:{electrons}:collision_frequencies:{electrons}_{positive_ions}_coll")); + } else { + setPermissions(readIfSet("species:{positive_ions}:temperature", Regions::Interior)); + } + if (electron_neutral) { + setPermissions(readOnly("species:{neutrals}:temperature", Regions::Interior)); + setPermissions(readWrite( + "species:{neutrals}:collision_frequencies:{neutrals}_{electrons}_coll")); + setPermissions(readWrite( + "species:{electrons}:collision_frequencies:{electrons}_{neutrals}_coll")); + } else { + setPermissions(readIfSet("species:{neutrals}:temperature", Regions::Interior)); + } + if (ion_ion) { + setPermissions(readWrite("species:{ions}:collision_frequencies:{ions}_{ions2}_coll")); + } + if (ion_neutral) { + setPermissions( + readWrite("species:{ions}:collision_frequencies:{ions}_{neutrals}_coll")); + setPermissions( + readWrite("species:{neutrals}:collision_frequencies:{neutrals}_{ions}_coll")); + } + if (neutral_neutral) { + setPermissions(readWrite( + "species:{neutrals}:collision_frequencies:{neutrals}_{neutrals2}_coll")); + } + if (electron_electron or electron_ion or electron_neutral) { + setPermissions(readWrite("species:{electrons}:collision_frequency")); + } + if (ion_ion or ion_neutral) { + setPermissions(readWrite("species:{ions}:collision_frequency")); + } else if (electron_ion) { + setPermissions(readWrite("species:{positive_ions}:collision_frequency")); + } + if (neutral_neutral or electron_neutral or ion_neutral) { + setPermissions(readWrite("species:{neutrals}:collision_frequency")); + } } /// Calculate apply collision data for the two species. @@ -69,7 +121,7 @@ BraginskiiCollisions::BraginskiiCollisions(const std::string& name, Options& all /// /// Note: A* variables are used for atomic mass numbers; /// mass* variables are species masses in kg -void BraginskiiCollisions::collide(Options& species1, Options& species2, +void BraginskiiCollisions::collide(GuardedOptions& species1, GuardedOptions& species2, const Field3D& nu_12) { AUTO_TRACE(); @@ -81,7 +133,7 @@ void BraginskiiCollisions::collide(Options& species1, Options& species2, set(collision_rates[species1.name()][species2.name()], nu_12); // Individual collision frequency used for diagnostics - if (&species1 != &species2) { + if (species1 != species2) { // For collisions between different species // m_a n_a \nu_{ab} = m_b n_b \nu_{ba} @@ -105,16 +157,16 @@ void BraginskiiCollisions::collide(Options& species1, Options& species2, } } -void BraginskiiCollisions::transform(Options& state) { +void BraginskiiCollisions::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; // Treat electron collisions specially // electron-ion and electron-neutral collisions if (allspecies.isSection("e")) { - Options& electrons = allspecies["e"]; + GuardedOptions electrons = allspecies["e"]; const Field3D Te = GET_NOBOUNDARY(Field3D, electrons["temperature"]) * Tnorm; // eV const Field3D Ne = GET_NOBOUNDARY(Field3D, electrons["density"]) * Nnorm; // In m^-3 @@ -152,7 +204,7 @@ void BraginskiiCollisions::transform(Options& state) { continue; } - Options& species = allspecies[kv.first]; // Note: Need non-const + GuardedOptions species = allspecies[kv.first]; // Note: Need non-const if (species.isSet("charge") and (get(species["charge"]) > 0.0)) { //////////////////////////////////// @@ -250,20 +302,19 @@ void BraginskiiCollisions::transform(Options& state) { // || species2 X X // \/ species3 X // - const std::map& children = allspecies.getChildren(); + const std::map children = allspecies.getChildren(); for (auto kv1 = std::begin(children); kv1 != std::end(children); ++kv1) { if (kv1->first == "e" or kv1->first == "ebeam") { continue; // Skip electrons } - Options& species1 = allspecies[kv1->first]; + GuardedOptions species1 = allspecies[kv1->first]; // If temperature isn't set, assume zero. in eV const Field3D temperature1 = species1.isSet("temperature") ? GET_NOBOUNDARY(Field3D, species1["temperature"]) * Tnorm : 0.0; - const Field3D density1 = GET_NOBOUNDARY(Field3D, species1["density"]) * Nnorm; const BoutReal AA1 = get(species1["AA"]); @@ -276,13 +327,13 @@ void BraginskiiCollisions::transform(Options& state) { // Copy the iterator, so we don't iterate over the // lower half of the matrix, but start at the diagonal - for (std::map::const_iterator kv2 = kv1; + for (std::map::const_iterator kv2 = kv1; kv2 != std::end(children); ++kv2) { if (kv2->first == "e" or kv2->first == "ebeam") { continue; // Skip electrons } - Options& species2 = allspecies[kv2->first]; + GuardedOptions species2 = allspecies[kv2->first]; // Note: Here species1 could be equal to species2 @@ -364,13 +415,14 @@ void BraginskiiCollisions::transform(Options& state) { // Copy the iterator, so we don't iterate over the // lower half of the matrix, but start at the diagonal - for (std::map::const_iterator kv2 = kv1; + for (std::map::const_iterator kv2 = kv1; kv2 != std::end(children); ++kv2) { + // FIXME: Should this include a check for "ebeam" too? if (kv2->first == "e") { continue; // Skip electrons } - Options& species2 = allspecies[kv2->first]; + GuardedOptions species2 = allspecies[kv2->first]; // Note: Here species1 could be equal to species2 diff --git a/src/braginskii_conduction.cxx b/src/braginskii_conduction.cxx index 4bc604c03..ff4f9a73a 100644 --- a/src/braginskii_conduction.cxx +++ b/src/braginskii_conduction.cxx @@ -26,7 +26,9 @@ using bout::globals::mesh; BraginskiiConduction::BraginskiiConduction(const std::string&, Options& alloptions, - Solver*) { + Solver*) + : Component({readIfSet("fields:Apar_flutter"), readOnly("species:{sp}:{input_vars}"), + readWrite("species:{sp}:{output_vars}")}) { AUTO_TRACE(); // Get settings for each species @@ -85,9 +87,35 @@ BraginskiiConduction::BraginskiiConduction(const std::string&, Options& alloptio "braginskii: self collisions and ie") .withDefault("multispecies"); } + + std::vector coll_types; + + substitutePermissions("input_vars", + {"AA", "density", "temperature", "pressure"}); + substitutePermissions("output_vars", + {"energy_source", "kappa_par", "energy_flow_ylow"}); + std::vector species; + for (const auto& [sp, mode] : all_conduction_collisions_mode) { + species.push_back(sp); + if (mode == "braginskii" and identifySpeciesType(sp) != SpeciesType::neutral) { + setPermissions(readIfSet( + fmt::format("species:{}:collision_frequencies:{}_{}_coll", sp, sp, sp))); + } else if (mode == "multispecies") { + setPermissions(readIfSet(fmt::format("species:{}:collision_frequencies:{}_{}_coll", + sp, sp, "{all_species}"))); + setPermissions(readIfSet(fmt::format("species:{}:collision_frequencies:{}_{}_cx", + sp, sp, "{all_species}"))); + } else if (mode == "AFN" and identifySpeciesType(sp) == SpeciesType::neutral) { + setPermissions(readIfSet(fmt::format("species:{}:collision_frequencies:{}_{}_cx", + sp, sp, "{positive_ions}"))); + setPermissions(readIfSet(fmt::format("species:{}:collision_frequencies:{}_{}_iz", + sp, sp, "{positive_ions}"))); + } + } + substitutePermissions("sp", species); } -void BraginskiiConduction::transform(Options& state) { +void BraginskiiConduction::transform_impl(GuardedOptions& state) { AUTO_TRACE(); for (auto& kv : state["species"].getChildren()) { @@ -97,8 +125,8 @@ void BraginskiiConduction::transform(Options& state) { continue; } /// Get the section containing this species - auto& species = state["species"][name]; - std::string const conduction_collisions_mode = all_conduction_collisions_mode[name]; + auto species = state["species"][name]; + const std::string conduction_collisions_mode = all_conduction_collisions_mode[name]; // Braginskii mode: plasma - self collisions and ei, neutrals - CX, IZ if (all_collision_names.count(name) == 0) { /// Calculate only once - at the beginning @@ -205,7 +233,7 @@ void BraginskiiConduction::transform(Options& state) { // FIXME: We end up applying these operations twice: here and in // EvolvePressure::finally - Field3D P = species["pressure"]; + Field3D P = GET_VALUE(Field3D, species["pressure"]); P.clearParallelSlices(); P.setBoundaryTo(get(species["pressure"])); Field3D const Pfloor = floor(P, 0.0); // Restricted to never go below zero diff --git a/src/braginskii_electron_viscosity.cxx b/src/braginskii_electron_viscosity.cxx index 4e3971514..beca6078c 100644 --- a/src/braginskii_electron_viscosity.cxx +++ b/src/braginskii_electron_viscosity.cxx @@ -19,7 +19,10 @@ #include "../include/component.hxx" BraginskiiElectronViscosity::BraginskiiElectronViscosity(const std::string& name, - Options& alloptions, Solver*) { + Options& alloptions, Solver*) + : Component({readIfSet("species:e:pressure"), readIfSet("species:e:velocity"), + readOnly("species:e:collision_frequency"), + readWrite("species:e:momentum_source")}) { auto& options = alloptions[name]; eta_limit_alpha = options["eta_limit_alpha"] @@ -29,10 +32,10 @@ BraginskiiElectronViscosity::BraginskiiElectronViscosity(const std::string& name diagnose = options["diagnose"].doc("Output diagnostics?").withDefault(false); } -void BraginskiiElectronViscosity::transform(Options& state) { +void BraginskiiElectronViscosity::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& species = state["species"]["e"]; + GuardedOptions species = state["species"]["e"]; if (!isSetFinal(species["pressure"], "electron_viscosity")) { throw BoutException("No electron pressure => Can't calculate electron viscosity"); diff --git a/src/braginskii_friction.cxx b/src/braginskii_friction.cxx index 19245be21..b812cff3c 100644 --- a/src/braginskii_friction.cxx +++ b/src/braginskii_friction.cxx @@ -13,7 +13,15 @@ #include "../include/component.hxx" BraginskiiFriction::BraginskiiFriction(const std::string& name, Options& alloptions, - Solver*) { + Solver*) + // FIXME: Not all species actually have collisions calculated + : Component({readOnly("species:{all_species}:density"), + readIfSet("species:{all_species}:velocity", Regions::Interior), + readOnly("species:{all_species}:AA"), + readIfSet("species:{all_species}:charge"), + readIfSet("species:{all_species}:collision_frequencies:{all_species}_{" + "all_species2}_coll"), + readWrite("species:{all_species}:momentum_source")}) { AUTO_TRACE(); Options& options = alloptions[name]; frictional_heating = options["frictional_heating"] @@ -21,6 +29,10 @@ BraginskiiFriction::BraginskiiFriction(const std::string& name, Options& allopti .withDefault(true); diagnose = options["diagnose"].doc("Output additional diagnostics?").withDefault(false); + + if (frictional_heating) { + setPermissions(readWrite("species:{all_species}:energy_source")); + } } BoutReal momentumCoefficient(BoutReal Zi) { @@ -38,10 +50,10 @@ BoutReal momentumCoefficient(const std::string& name1, BoutReal Z1, return 1.; } -void BraginskiiFriction::transform(Options& state) { +void BraginskiiFriction::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; // Iterate through all species // To avoid double counting, this needs to iterate over pairs @@ -55,9 +67,9 @@ void BraginskiiFriction::transform(Options& state) { // || species2 X X // \/ species3 X // - const std::map& children = allspecies.getChildren(); + const std::map children = allspecies.getChildren(); for (auto kv1 = std::begin(children); kv1 != std::end(children); ++kv1) { - Options& species1 = allspecies[kv1->first]; + GuardedOptions species1 = allspecies[kv1->first]; // If collisions were not calculated for this species, skip it. if (not species1.isSection("collision_frequencies")) { continue; @@ -80,7 +92,7 @@ void BraginskiiFriction::transform(Options& state) { continue; } - Options& species2 = allspecies[kv2->first]; + GuardedOptions species2 = allspecies[kv2->first]; // At least one of the species must have a velocity for there to be friction. if (!(isSetFinalNoBoundary(species1["velocity"]) diff --git a/src/braginskii_heat_exchange.cxx b/src/braginskii_heat_exchange.cxx index 14cb211cd..ad655c572 100644 --- a/src/braginskii_heat_exchange.cxx +++ b/src/braginskii_heat_exchange.cxx @@ -13,17 +13,29 @@ #include "../include/component.hxx" BraginskiiHeatExchange::BraginskiiHeatExchange(const std::string& name, - Options& alloptions, Solver*) { + Options& alloptions, Solver*) + // FIXME: Not all species are actually read or written; only those with collision + // rates and temperatures + : Component({readOnly("species:{all_species}:{input_vars}"), + readIfSet("species:{all_species}:{optional_vars}"), + readWrite("species:{all_species}:{output_vars}")}) { AUTO_TRACE(); diagnose = alloptions[name]["diagnose"] .doc("Output additional diagnostics?") .withDefault(false); + substitutePermissions("input_vars", {"AA", "density"}); + // FIXME: We don't access the self-collision rate + substitutePermissions("optional_vars", + {"charge", + "collision_frequencies:{all_species}_{all_species2}_coll", + "temperature"}); + substitutePermissions("output_vars", {"momentum_source", "energy_source"}); } -void BraginskiiHeatExchange::transform(Options& state) { +void BraginskiiHeatExchange::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; // Iterate through all species // To avoid double counting, this needs to iterate over pairs @@ -37,9 +49,9 @@ void BraginskiiHeatExchange::transform(Options& state) { // || species2 X X // \/ species3 X // - const std::map& children = allspecies.getChildren(); + const std::map children = allspecies.getChildren(); for (auto kv1 = std::begin(children); kv1 != std::end(children); ++kv1) { - Options& species1 = allspecies[kv1->first]; + GuardedOptions species1 = allspecies[kv1->first]; // If collisions were not calculated for this species, skip it. if (not species1.isSection("collision_frequencies")) { continue; @@ -54,16 +66,17 @@ void BraginskiiHeatExchange::transform(Options& state) { // Copy the iterator, so we don't iterate over the // lower half of the matrix, but start at the diagonal - for (std::map::const_iterator kv2 = kv1; + for (std::map::const_iterator kv2 = kv1; kv2 != std::end(children); ++kv2) { // Can't have heat exchange with oneself if (kv1->first == kv2->first) { continue; } - Options& species2 = allspecies[kv2->first]; + GuardedOptions species2 = allspecies[kv2->first]; - // At least one of the species must have a velocity for there to be friction. + // At least one of the species must have a temperature for there to be heat + // exchange. if (!(species1.isSet("temperature") or species2.isSet("temperature"))) { continue; } diff --git a/src/braginskii_ion_viscosity.cxx b/src/braginskii_ion_viscosity.cxx index 780df2018..6b8fe23fc 100644 --- a/src/braginskii_ion_viscosity.cxx +++ b/src/braginskii_ion_viscosity.cxx @@ -30,7 +30,16 @@ using bout::globals::mesh; BraginskiiIonViscosity::BraginskiiIonViscosity(const std::string& name, - Options& alloptions, Solver*) { + Options& alloptions, Solver*) + : Component({ + readIfSet("species:{non_electrons}:pressure"), + readIfSet("species:{non_electrons}:velocity"), + readIfSet("species:{non_electrons}:charge"), + readIfSet("species:{non_electrons}:collision_frequencies:{coll_type}"), + readWrite("species:{non_electrons}:momentum_source"), + readWrite("species:{non_electrons}:energy_source"), + readWrite("fields:DivJextra"), + }) { auto& options = alloptions[name]; eta_limit_alpha = options["eta_limit_alpha"] @@ -107,12 +116,24 @@ BraginskiiIonViscosity::BraginskiiIonViscosity(const std::string& name, const BoutReal Lnorm = units["meters"]; bounce_frequency_R /= Lnorm; } + + std::vector coll_types; + if (viscosity_collisions_mode == "braginskii") { + coll_types.push_back("{non_electrons}_{non_electrons}_coll"); + } else if (viscosity_collisions_mode == "multispecies") { + coll_types.push_back("{non_electrons}_{all_species}_coll"); + coll_types.push_back("{non_electrons}_{all_species}_cx"); + } + if (perpendicular) { + setPermissions(readOnly("fields:phi")); + } + substitutePermissions("coll_type", coll_types); } -void BraginskiiIonViscosity::transform(Options& state) { +void BraginskiiIonViscosity::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; auto coord = mesh->getCoordinates(); const Field2D Bxy = coord->Bxy; @@ -126,7 +147,7 @@ void BraginskiiIonViscosity::transform(Options& state) { } const auto& species_name = kv.first; - Options& species = allspecies[species_name]; + GuardedOptions species = allspecies[species_name]; if (!(isSetFinal(species["pressure"], "ion_viscosity") and isSetFinal(species["velocity"], "ion_viscosity") @@ -150,10 +171,10 @@ void BraginskiiIonViscosity::transform(Options& state) { if (viscosity_collisions_mode == "braginskii") { for (const auto& collision : species["collision_frequencies"].getChildren()) { - std::string const collision_name = collision.second.name(); + const std::string collision_name = collision.first; if ( // Self-collisions - (collisionSpeciesMatch(collision_name, species.name(), species.name(), + (collisionSpeciesMatch(collision_name, species_name, species_name, "coll", "exact"))) { collision_names[species_name].push_back(collision_name); } @@ -162,13 +183,13 @@ void BraginskiiIonViscosity::transform(Options& state) { } else if (viscosity_collisions_mode == "multispecies") { for (const auto& collision : species["collision_frequencies"].getChildren()) { - std::string const collision_name = collision.second.name(); + const std::string collision_name = collision.first; if ( // Charge exchange - (collisionSpeciesMatch(collision_name, species.name(), "", "cx", "partial")) + (collisionSpeciesMatch(collision_name, species_name, "", "cx", "partial")) or // Any collision (en, in, ee, ii, nn) - (collisionSpeciesMatch(collision_name, species.name(), "", "coll", + (collisionSpeciesMatch(collision_name, species_name, "", "coll", "partial"))) { collision_names[species_name].push_back(collision_name); @@ -177,18 +198,18 @@ void BraginskiiIonViscosity::transform(Options& state) { } else { throw BoutException("\tviscosity_collisions_mode for {:s} must be either " "multispecies or braginskii", - species.name()); + species_name); } if (collision_names[species_name].empty()) { throw BoutException("\tNo collisions found for {:s} in ion_viscosity for " "selected collisions mode", - species.name()); + species_name); } // Write chosen collisions to log file output_info.write("\t{:s} viscosity collisionality mode: '{:s}' using ", - species.name(), viscosity_collisions_mode); + species_name, viscosity_collisions_mode); for (const auto& collision : collision_names[species_name]) { output_info.write("{:s} ", collision); } diff --git a/src/braginskii_thermal_force.cxx b/src/braginskii_thermal_force.cxx index 2786e5f61..62108dd98 100644 --- a/src/braginskii_thermal_force.cxx +++ b/src/braginskii_thermal_force.cxx @@ -14,15 +14,15 @@ #include "../include/braginskii_thermal_force.hxx" #include "../include/component.hxx" -void BraginskiiThermalForce::transform(Options& state) { +void BraginskiiThermalForce::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; if (electron_ion && allspecies.isSection("e")) { // Electron-ion collisions - Options& electrons = allspecies["e"]; + GuardedOptions electrons = allspecies["e"]; // Need Te boundary to take gradient const Field3D Te = GET_VALUE(Field3D, electrons["temperature"]); const Field3D Grad_Te = Grad_par(Te); @@ -31,7 +31,7 @@ void BraginskiiThermalForce::transform(Options& state) { if (kv.first == "e") { continue; // Omit electron-electron } - Options& species = allspecies[kv.first]; + GuardedOptions species = allspecies[kv.first]; if (!species.isSet("charge") or get(species["charge"]) == 0) { continue; // Only considering charged particle interactions @@ -61,9 +61,9 @@ void BraginskiiThermalForce::transform(Options& state) { // || species2 X // \/ species3 // - const std::map& children = allspecies.getChildren(); + const std::map children = allspecies.getChildren(); for (auto kv1 = std::begin(children); kv1 != std::end(children); ++kv1) { - Options& species1 = allspecies[kv1->first]; + GuardedOptions species1 = allspecies[kv1->first]; if (kv1->first == "e" or !species1.isSet("charge") or get(species1["charge"]) == 0) { @@ -72,9 +72,9 @@ void BraginskiiThermalForce::transform(Options& state) { // Copy the iterator, so we don't iterate over the // lower half of the matrix or the diagonal but start the diagonal - for (std::map::const_iterator kv2 = std::next(kv1); + for (std::map::const_iterator kv2 = std::next(kv1); kv2 != std::end(children); ++kv2) { - Options& species2 = allspecies[kv2->first]; + GuardedOptions species2 = allspecies[kv2->first]; if (kv2->first == "e" or !species2.isSet("charge") or get(species2["charge"]) == 0) { @@ -84,8 +84,8 @@ void BraginskiiThermalForce::transform(Options& state) { // Now have two different ion species, species1 and species2 // Only including one majority light species, and one trace heavy species - Options* light; - Options* heavy; + GuardedOptions* light; + GuardedOptions* heavy; if ((get(species1["AA"]) < 4) and (get(species2["AA"]) > 10)) { // species1 light, species2 heavy diff --git a/src/classical_diffusion.cxx b/src/classical_diffusion.cxx index eb95780e3..6bf7270e3 100644 --- a/src/classical_diffusion.cxx +++ b/src/classical_diffusion.cxx @@ -2,7 +2,10 @@ #include -ClassicalDiffusion::ClassicalDiffusion(std::string name, Options& alloptions, Solver*) { +ClassicalDiffusion::ClassicalDiffusion(std::string name, Options& alloptions, Solver*) + : Component({readIfSet("species:{all_species}:{optional}"), + readOnly("species:e:{e_vals}"), + readWrite("species:{all_species}:{output}")}) { AUTO_TRACE(); Options& options = alloptions[name]; @@ -10,20 +13,35 @@ ClassicalDiffusion::ClassicalDiffusion(std::string name, Options& alloptions, So diagnose = options["diagnose"].doc("Output additional diagnostics?").withDefault(false); custom_D = options["custom_D"].doc("Custom diffusion coefficient override. -1: Off, calculate D normally").withDefault(-1); + + substitutePermissions("optional", + {"charge", "pressure", "density", "velocity", "temperature"}); + std::vector e_vals = {"AA", "density"}; + if (custom_D <= 0.) { + e_vals.push_back("collision_frequency"); + } + substitutePermissions("e_vals", e_vals); + // FIXME: momentum and energy sources are only set if velocity and + // temperature are defined (respectively). Collision frequency is + // only used if temperature is set. Nothing happens if the charge or + // density are unset. + substitutePermissions("output", {"density_source", "momentum_source", "energy_source"}); + if (custom_D < 0.) + setPermissions(readOnly("species:{all_species}:collision_frequency")); } -void ClassicalDiffusion::transform(Options &state) { +void ClassicalDiffusion::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; // Particle diffusion coefficient // The only term here comes from the resistive drift Field3D Ptotal = 0.0; for (auto& kv : allspecies.getChildren()) { - const auto& species = kv.second; + const auto species = kv.second; - if (!(species.isSet("charge") and IS_SET(species["pressure"]))) { + if (!(species.isSet("charge") and species.isSet("pressure"))) { continue; // Skip, go to next species } auto q = get(species["charge"]); @@ -33,7 +51,7 @@ void ClassicalDiffusion::transform(Options &state) { Ptotal += GET_VALUE(Field3D, species["pressure"]); } - auto& electrons = allspecies["e"]; + auto electrons = allspecies["e"]; const auto me = get(electrons["AA"]); const Field3D Ne = GET_VALUE(Field3D, electrons["density"]); @@ -52,10 +70,10 @@ void ClassicalDiffusion::transform(Options &state) { Dn[i] = 0.0; } - for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: Need non-const + for (auto kv : allspecies.getChildren()) { + GuardedOptions species = allspecies[kv.first]; // Note: Need non-const - if (!(species.isSet("charge") and IS_SET(species["density"]))) { + if (!(species.isSet("charge") and species.isSet("density"))) { continue; // Skip, go to next species } auto q = get(species["charge"]); diff --git a/src/component.cxx b/src/component.cxx index 07d8a6182..c53cceb83 100644 --- a/src/component.cxx +++ b/src/component.cxx @@ -1,5 +1,19 @@ +#include +#include + +#include +#include +#include #include "../include/component.hxx" +#include "../include/guarded_options.hxx" +#include "../include/permissions.hxx" + +auto fmt::formatter::format(const ComponentInformation& ci, format_context& ctx) const + -> format_context::iterator { + return formatter::format( + fmt::format("{} ({})", ci.name, ci.type), ctx); +} std::unique_ptr Component::create(const std::string &type, const std::string &name, @@ -9,6 +23,41 @@ std::unique_ptr Component::create(const std::string &type, return ComponentFactory::getInstance().create(type, name, alloptions, solver); } +void Component::transform(Options& state) { + GuardedOptions guarded(&state, &state_variable_access); + transform_impl(guarded); +#if CHECKLEVEL >= 1 + for (auto& [varname, region] : guarded.unreadItems()) { + output_warn.write("Did not read from state variable {} in region(s) {}\n", varname, + Permissions::regionNames(region)); + } + for (auto& [varname, region] : guarded.unwrittenItems()) { + output_warn.write("Did not write to state variable {} in region(s) {}\n", varname, + Permissions::regionNames(region)); + } +#endif +} + +void Component::declareAllSpecies(const SpeciesInformation & info) { + state_variable_access.substitute("electrons", info.electrons); + state_variable_access.substitute("electrons2", info.electrons); + state_variable_access.substitute("neutrals", info.neutrals); + state_variable_access.substitute("neutrals2", info.neutrals); + state_variable_access.substitute("positive_ions", info.positive_ions); + state_variable_access.substitute("positive_ions2", info.positive_ions); + state_variable_access.substitute("negative_ions", info.negative_ions); + state_variable_access.substitute("negative_ions2", info.negative_ions); + state_variable_access.substitute("ions", info.ions); + state_variable_access.substitute("ions2", info.ions); + state_variable_access.substitute("charged", info.charged); + state_variable_access.substitute("charged", info.charged); + state_variable_access.substitute("non_electrons", info.non_electrons); + state_variable_access.substitute("non_electrons2", info.non_electrons); + state_variable_access.substitute("all_species", info.all_species); + state_variable_access.substitute("all_species2", info.all_species); + state_variable_access.checkNoRemainingSubstitutions(); +} + constexpr decltype(ComponentFactory::type_name) ComponentFactory::type_name; constexpr decltype(ComponentFactory::section_name) ComponentFactory::section_name; constexpr decltype(ComponentFactory::option_name) ComponentFactory::option_name; @@ -23,6 +72,21 @@ bool isSetFinal(const Options& option, [[maybe_unused]] const std::string& locat return option.isSet(); } +bool isSetFinal(const GuardedOptions & option, [[maybe_unused]] const std::string& location) { + const bool set = option.isSet(); +#if CHECKLEVEL >= 1 + const PermissionTypes perm = option.getHighestPermission(); + if (perm >= PermissionTypes::Read + or (perm == PermissionTypes::ReadIfSet and set)) { + const Options& opt = option.get(); + const_cast(opt).attributes["final"] = location; + const_cast(opt).attributes["final-domain"] = location; + } +#endif + return set; +} + + bool isSetFinalNoBoundary(const Options& option, [[maybe_unused]] const std::string& location) { #if CHECKLEVEL >= 1 // Mark option as final inside the domain, but not in the boundary @@ -30,3 +94,16 @@ bool isSetFinalNoBoundary(const Options& option, [[maybe_unused]] const std::str #endif return option.isSet(); } + +bool isSetFinalNoBoundary(const GuardedOptions & option, [[maybe_unused]] const std::string& location) { + const bool set = option.isSet(); +#if CHECKLEVEL >= 1 + const PermissionTypes perm = option.getHighestPermission(Regions::Interior); + if (perm >= PermissionTypes::Read or (perm == PermissionTypes::ReadIfSet and set)) { + // Mark option as final inside the domain, but not in the boundary + const_cast(option.get(Regions::Interior)).attributes["final-domain"] = + location; + } +#endif + return set; +} diff --git a/src/component_scheduler.cxx b/src/component_scheduler.cxx index 9f3c0c48c..4222fa867 100644 --- a/src/component_scheduler.cxx +++ b/src/component_scheduler.cxx @@ -1,14 +1,346 @@ -#include "../include/component_scheduler.hxx" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include // for trim, strsplit +#include +#include + +#include "../include/component.hxx" +#include "../include/component_scheduler.hxx" +#include "../include/permissions.hxx" + +const std::set ComponentScheduler::predeclared_variables = { + "time", "linear", "units:inv_meters_cubed", "units:eV", "units:Tesla", + "units:seconds", "units:meters"}; + +/// Perform a depth-first topological sort, starting from `item`. It +/// will finish once it reaches the end of `item`'s dependency chain, +/// so this needs to be called in a loop for all items. Information +/// about `item` is stored in the corresponding index of the vector +/// arguments. +/// +/// In pratice, `item` here represents the index of a particular +/// component. The indices of the dependencies of `item` are stored in +/// the corresponding element of `dependencies`. +void topological_sort(const std::vector>& dependencies, size_t item, + std::vector& sorted, std::vector& processing, + std::vector& processed) { + if (processed[item]) { + return; + } + if (processing[item]) { + throw BoutException("Circular dependency among components."); + } + processing[item] = true; + + for (const auto dep : dependencies[item]) { + topological_sort(dependencies, dep, sorted, processing, processed); + } + processed[item] = true; + sorted.push_back(item); +} + +/// Get all the parent sections of a variable "path". Sections are +/// separated by colons in the path. +std::set getParents(const std::string& name) { + std::set result; + size_t start = 0; + size_t position = name.find(":", start); + while (position != std::string::npos) { + result.insert(name.substr(0, position)); + start = position + 1; + position = name.find(":", start); + } + return result; +} + +/// Produce a map between Option paths and all variable names held +/// within that path. If the path refers to a section then it maps to +/// the set of all variables contained in that section and any +/// sub-sections. Otherwise the path corresponds to a variable and +/// just maps to itself. Only paths which are explicitly given a +/// permission by at least one component will be present. +std::map> +getVariableHierarchy(const std::vector>& components) { + // Build up a set of all variable names which are read only if they + // are set by another component + std::set conditional_names; + for (const auto& component : components) { + const Permissions& permissions = component->getPermissions(); + for (const auto& [varname, _] : + permissions.getVariablesWithPermission(PermissionTypes::ReadIfSet, true)) { + conditional_names.insert(varname); + } + } + + // Build up a set of all section/variable names which are definitely + // read/written by components, and the sections which they imply + // exist + std::set unconditional_names; + std::set unconditional_sections; + for (const auto& component : components) { + const Permissions& permissions = component->getPermissions(); + for (const auto& [varname, _] : + permissions.getVariablesWithPermission(PermissionTypes::Read, false)) { + unconditional_names.insert(varname); + unconditional_sections.merge(getParents(varname)); + } + } + + /// Assemble the set of all section names which are referred to + /// explicitly in the component permissions. + std::set sections_present; + std::set_intersection(unconditional_names.begin(), unconditional_names.end(), + unconditional_sections.begin(), unconditional_sections.end(), + std::inserter(sections_present, sections_present.begin())); + /// Assemble the set of all variable names which are definitlye + /// read/written by components (i.e., not including sections) + std::set non_sections; + std::set_difference(unconditional_names.begin(), unconditional_names.end(), + sections_present.begin(), sections_present.end(), + std::inserter(non_sections, non_sections.begin())); + + std::map> result; + + // ReadIfSet variables will only actually be used if they are + // reference elsewhere. We create them with empty sets, which will + // get filled if they are present. + for (const auto& name : conditional_names) { + // FIXME: this isn't an empty set + //result.insert({name, {name}}); + result.insert({name, {}}); + } + + // Non-sections map to themselves + for (const auto& name : non_sections) { + result[name] = {name}; + } + // Sections map to those variables which they contain + for (const auto& section : sections_present) { + auto& children = result[section]; + const std::string sec_suffixed = section + ':'; + for (const auto& name : non_sections) { + if (name.rfind(sec_suffixed, 0) == 0) { + children.insert(name); + } + } + } + + return result; +} + +/// Get all variables to which the name could be referring (e.g., its +/// children if it is a section name). These will be filtered to +/// remove any variables for which more specific permissions are +/// given. +std::set +expandVariableName(const std::map>& hierarchy, + const Permissions& permissions, const std::string& name) { + const std::set& candidates = hierarchy.at(name); + std::set result; + // Only return the values that do not have a more specific permission + std::copy_if(candidates.begin(), candidates.end(), + std::inserter(result, result.begin()), + [&permissions, &name](const std::string& candidate) -> bool { + return permissions.bestMatchRights(candidate).name == name; + }); + return result; +} + +using Var = std::pair; + +/// Create a map between a variable and the set of components that +/// access it with the specified permission level. +std::map> +getPermissionComponentMap(const std::vector>& components, + const std::map>& hierarchy, + PermissionTypes permission) { + std::map> result; + for (size_t i = 0; i < components.size(); i++) { + const Permissions& permissions = components[i]->getPermissions(); + for (const auto& [name, regions] : + permissions.getVariablesWithPermission(permission)) { + for (const auto& sub_name : expandVariableName(hierarchy, permissions, name)) { + for (const auto& [region, _] : Permissions::fundamental_regions) { + if ((regions & region) == region) { + result[{sub_name, region}].insert(i); + } + } + } + } + } + return result; +} + +/// Modifies component_dependencies to include information on which +/// components depend on each other. It does this by making components +/// which read a variable depend on whichever component(s) write that +/// variable (information contained in the `writers` +/// argument). Returns a set of any read variables which are not +/// written by any component. +std::set +setReadDependencies(const std::vector>& components, + const std::map>& hierarchy, + const std::map>& writers, + PermissionTypes permission, + std::vector>& component_dependencies) { + std::set missing; + for (size_t i = 0; i < components.size(); i++) { + const Permissions& permissions = components[i]->getPermissions(); + // Create dependencies between components that read variables and those that write + // them + for (const auto& [name, regions] : + permissions.getVariablesWithPermission(permission)) { + if (ComponentScheduler::predeclared_variables.count(name) > 0) { + continue; + } + for (const auto& sub_name : expandVariableName(hierarchy, permissions, name)) { + for (const auto& [region, _] : Permissions::fundamental_regions) { + if ((regions & region) == region) { + const auto item = writers.find({sub_name, region}); + if (item == writers.end()) { + missing.insert( + fmt::format("{} ({})", sub_name, Permissions::regionNames(region))); + } else { + component_dependencies[i].insert(item->second.begin(), item->second.end()); + } + } + } + } + } + } + return missing; +} + + +void printComponents(const std::vector& component_order) { + if (component_order.size() > 0) { + output_info << "Components will be executed in the following order:\n"; + } + for (const auto & comp : component_order) { + output_info << fmt::format("\t{}\n", comp); + } +} + +/// Topologically sorts the list of components to ensure variables are +/// written and read in the right order. +void sortComponents(std::vector>& components, const std::vector component_info) { + // Map between variable/section names specified by component + // permissions and the variables they contain. In the case of + // sections this is all variables within the section and any + // sub-sections. Non-section viarables map to themselves. + const std::map> variable_hierarchy = + getVariableHierarchy(components); + + // Get information on which components write each variable + std::map> nonfinal_writes = + getPermissionComponentMap(components, variable_hierarchy, PermissionTypes::Write); + std::map> final_writes = + getPermissionComponentMap(components, variable_hierarchy, PermissionTypes::Final); + + // Object mapping between components (reprsented by the index of + // that component in the `components` argument) and the components + // each of these depends upon (represented by a set of the indices + // for those components). + std::vector> component_dependencies(components.size()); + + // Components which do a final write on a variable depend on all + // components which do non-final writes on that variable + for (const auto& [var, comp_indices] : final_writes) { + if (comp_indices.size() > 1) { + throw BoutException( + "Multiple components have permission to make final write to variable {}", var); + } + for (const size_t i : comp_indices) { + const auto item = nonfinal_writes.find(var); + if (item != nonfinal_writes.end()) { + // Note that calling merge actually removes the items from the + // sets stored in nonfinal_writes. This is fine because the + // only remaining thing for which we will use nonfinal_writes + // is setting up variable_writers and that doesn't use any + // information on variables which have a final + // write. Therefore it won't do any harm hear to remove + // information about variables which have a final write.. + component_dependencies[i].merge(item->second); + } + } + } + + // Work out which component(s) last write a variable before it may + // be read. For variables with a final-write, it is whichever + // component performs that final write. + std::map> variable_writers = std::move(final_writes); + // For other variables, it is the set of all components which have + // write permission. + variable_writers.merge(std::move(nonfinal_writes)); + + // Insert dependency information for components that (unconditionally) read variables + std::set missing = + setReadDependencies(components, variable_hierarchy, variable_writers, + PermissionTypes::Read, component_dependencies); + if (missing.size() > 0) { + throw BoutException( + "The following required variables are not written by any component:\n\t{}\n", + fmt::format("{}", fmt::join(missing, "\n\t"))); + } + // Insert dependency information for components that read variables + // if those variables have been set. If can not find a place where + // the variable is written, it will just be skipped. + setReadDependencies(components, variable_hierarchy, variable_writers, + PermissionTypes::ReadIfSet, component_dependencies); + + // Create ancillary variables for sorting process + std::vector processing(components.size(), false); + std::vector processed(components.size(), false); + std::vector order; + + // Perform the sort + for (size_t i = 0; i < components.size(); i++) { + if (!processed[i]) { + topological_sort(component_dependencies, i, order, processing, processed); + } + } + + std::vector component_order(component_info.size()); + + // Create the result with components in the desired order + std::vector> + result(components.size()); + for (size_t i = 0; i < components.size(); i++) { + component_order[i] = component_info[order[i]]; + std::swap(result[i], components[order[i]]); + } + + components = std::move(result); + printComponents(component_order); +} ComponentScheduler::ComponentScheduler(Options &scheduler_options, Options &component_options, Solver *solver) { - std::string component_names = scheduler_options["components"] - .doc("Components in order of execution") - .as(); + const std::string component_names = scheduler_options["components"] + .doc("Components in order of execution") + .as(); + const bool autosort = scheduler_options["autosort"].doc("Perform a topological sort to ensure components executed in the right order?").withDefault(true); + + std::list required_components; + + std::vector electrons; + std::vector neutrals; + std::vector positive_ions; + std::vector negative_ions; // For now split on ','. Something like "->" might be better for (const auto &name : strsplit(component_names, ',')) { @@ -20,11 +352,32 @@ ComponentScheduler::ComponentScheduler(Options &scheduler_options, continue; } + if (name_trimmed == "e" or name == "ebeam") { + electrons.push_back(name_trimmed); + } + // FIXME: Would there be any spcies without AA? Is there any other + // reliable way to identify what is a species? + else if (component_options[name_trimmed].isSet("AA")) { + if (component_options[name_trimmed].isSet("charge")) { + const BoutReal charge = component_options[name_trimmed]["charge"]; + if (charge > 1e-5) { + positive_ions.push_back(name_trimmed); + } else if (charge < -1e-5) { + negative_ions.push_back(name_trimmed); + } else { + neutrals.push_back(name_trimmed); + } + } else { + neutrals.push_back(name_trimmed); + } + } + // For each component e.g. "e", several Component types can be created // but if types are not specified then the component name is used - std::string types = component_options[name_trimmed].isSet("type") - ? component_options[name_trimmed]["type"].as() - : name_trimmed; + const std::string types = + component_options[name_trimmed].isSet("type") + ? component_options[name_trimmed]["type"].as() + : name_trimmed; for (const auto &type : strsplit(types, ',')) { auto type_trimmed = trim(type, " \t\r()"); @@ -32,12 +385,53 @@ ComponentScheduler::ComponentScheduler(Options &scheduler_options, continue; } - components.push_back(Component::create(type_trimmed, - name_trimmed, - component_options, - solver)); + required_components.emplace_back(name_trimmed, type_trimmed); } } + + // Use sets for efficient lookup of whether a component is already + // in the queue to be created or already has been created. + // + // We use a vector to decide the order in which to create + // components, to keep this close to what is in teh file. + std::set unbuilt_components(required_components.begin(), required_components.end()); + std::set created_components; + // We use a vector to keep track of the actual order in which + // components are created + std::vector component_order; + + // FIXME: This is a hack, so I can debug the failing case + if (autosort) { + required_components = std::list(unbuilt_components.begin(), unbuilt_components.end()); + } + while (required_components.size() > 0) { + const ComponentInformation component = required_components.front(); + required_components.pop_front(); + auto comp = Component::create(component.type, component.name, component_options, solver); + std::vector sub_components = comp->additionalComponents(); + for (auto sub_comp = sub_components.rbegin(); sub_comp != sub_components.rend(); ++sub_comp) { + if (unbuilt_components.count(*sub_comp) == 0 + and created_components.count(*sub_comp) == 0) { + required_components.push_front(*sub_comp); + unbuilt_components.insert(*sub_comp); + } + } + component_order.push_back(component); + components.push_back(std::move(comp)); + created_components.insert(unbuilt_components.extract(component)); + } + + const SpeciesInformation species(electrons, neutrals, positive_ions, negative_ions); + + for (auto& component : components) { + component->declareAllSpecies(species); + } + + if (autosort) { + sortComponents(components, component_order); + } else { + printComponents(component_order); + } } std::unique_ptr ComponentScheduler::create(Options &scheduler_options, diff --git a/src/detachment_controller.cxx b/src/detachment_controller.cxx index d2d6004a0..a68c927ff 100644 --- a/src/detachment_controller.cxx +++ b/src/detachment_controller.cxx @@ -31,7 +31,7 @@ BoutReal calculateGradient(const std::vector& x, const std::vectoriterateBndryUpperY(); !r.isDone(); r++) { Curlb_B.y(r.ind, mesh->yend + 1) = -Curlb_B.y(r.ind, mesh->yend); } + + // FIXME: density, pressure, and momentum will not be read even if + // they are defined if charge and temperature were not defined for + // that species. + substitutePermissions("input", + {"charge", "temperature", "density", "pressure", "momentum"}); + // FIXME: These will actually only be written if density, pressure, + // and momentum are set, respectively. They also require charge and + // temperature to have been set. + substitutePermissions("output", {"density_source", "energy_source", "momentum_source"}); } -void DiamagneticDrift::transform(Options& state) { +void DiamagneticDrift::transform_impl(GuardedOptions& state) { // Iterate through all subsections - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: Need non-const + GuardedOptions species = allspecies[kv.first]; // Note: Need non-const if (!(species.isSet("charge") and species.isSet("temperature"))) continue; // Skip, go to next species diff --git a/src/electromagnetic.cxx b/src/electromagnetic.cxx index 239a966fc..464ad5001 100644 --- a/src/electromagnetic.cxx +++ b/src/electromagnetic.cxx @@ -13,7 +13,13 @@ BOUT_OVERRIDE_DEFAULT_OPTION("electromagnetic:laplacian:rtol_accept", 1e-2); BOUT_OVERRIDE_DEFAULT_OPTION("electromagnetic:laplacian:atol_accept", 1e-6); BOUT_OVERRIDE_DEFAULT_OPTION("electromagnetic:laplacian:maxits", 1000); -Electromagnetic::Electromagnetic(std::string name, Options &alloptions, Solver* solver) { +Electromagnetic::Electromagnetic(std::string name, Options& alloptions, Solver* solver) + : Component({readIfSet("species:{all_species}:charge"), + writeFinal("species:{all_species}:momentum"), + writeFinal("species:{all_species}:velocity"), readOnly("time"), + readOnly("species:{all_species}:AA"), + readOnly("species:{all_species}:density", Regions::Interior), + readWrite("fields:Apar")}) { AUTO_TRACE(); Options& units = alloptions["units"]; @@ -79,6 +85,9 @@ Electromagnetic::Electromagnetic(std::string name, Options &alloptions, Solver* magnetic_flutter = options["magnetic_flutter"] .doc("Set magnetic flutter terms (Apar_flutter)?") .withDefault(false); + + if (magnetic_flutter) + setPermissions(readWrite("fields:Apar_flutter")); } void Electromagnetic::restartVars(Options& state) { @@ -102,10 +111,10 @@ void Electromagnetic::restartVars(Options& state) { {"source", "electromagnetic"}}); } -void Electromagnetic::transform(Options &state) { +void Electromagnetic::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; // Sum coefficients over species // @@ -113,7 +122,7 @@ void Electromagnetic::transform(Options &state) { alpha_em = 0.0; Ajpar = 0.0; for (auto& kv : allspecies.getChildren()) { - const Options& species = kv.second; + const GuardedOptions species = kv.second; if (!species.isSet("charge") or !species.isSet("momentum")) { continue; // Not charged, or no parallel flow @@ -184,7 +193,7 @@ void Electromagnetic::transform(Options &state) { // Update momentum for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: need non-const + GuardedOptions species = allspecies[kv.first]; // Note: need non-const if (!species.isSet("charge") or !species.isSet("momentum")) { continue; // Not charged, or no parallel flow diff --git a/src/electron_force_balance.cxx b/src/electron_force_balance.cxx index a097fa70b..a2e32087b 100644 --- a/src/electron_force_balance.cxx +++ b/src/electron_force_balance.cxx @@ -5,17 +5,17 @@ using bout::globals::mesh; -void ElectronForceBalance::transform(Options &state) { +void ElectronForceBalance::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - if (IS_SET(state["fields"]["phi"])) { + if (state["fields"].isSet("phi")) { // Here we use electron force balance to calculate the parallel electric field // rather than the electrostatic potential throw BoutException("Cannot calculate potential and use electron force balance\n"); } // Get the electron pressure, with boundary condition applied - Options& electrons = state["species"]["e"]; + GuardedOptions electrons = state["species"]["e"]; Field3D Pe = GET_VALUE(Field3D, electrons["pressure"]); Field3D Ne = GET_NOBOUNDARY(Field3D, electrons["density"]); @@ -34,14 +34,14 @@ void ElectronForceBalance::transform(Options &state) { Epar = force_density / floor(Ne, 1e-5); // Now calculate forces on other species - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (auto& kv : allspecies.getChildren()) { if (kv.first == "e") { continue; // Skip electrons } - Options& species = allspecies[kv.first]; // Note: Need non-const + GuardedOptions species = allspecies[kv.first]; // Note: Need non-const - if (!(IS_SET(species["density"]) and IS_SET(species["charge"]))) { + if (!(species.isSet("density") and species.isSet("charge"))) { continue; // Needs both density and charge to experience a force } diff --git a/src/evolve_density.cxx b/src/evolve_density.cxx index 672d1ea6b..f4dbda028 100644 --- a/src/evolve_density.cxx +++ b/src/evolve_density.cxx @@ -15,7 +15,7 @@ using bout::globals::mesh; EvolveDensity::EvolveDensity(std::string name, Options& alloptions, Solver* solver) - : name(name) { + : Component({readWrite("species:{name}:{outputs}")}), name(name) { AUTO_TRACE(); auto& options = alloptions[name]; @@ -130,9 +130,23 @@ EvolveDensity::EvolveDensity(std::string name, Options& alloptions, Solver* solv neumann_boundary_average_z = alloptions[std::string("N") + name]["neumann_boundary_average_z"] .doc("Apply neumann boundary with Z average?") .withDefault(false); + + std::vector outputs = {"AA", "density", "density_source"}; + if (charge != 0.) { + outputs.push_back("charge"); + } + if (low_n_diffuse) { + outputs.push_back("low_n_coeff"); + } + + if (source_time_dependent) { + setPermissions(readOnly("time")); + } + substitutePermissions("name", {name}); + substitutePermissions("outputs", outputs); } -void EvolveDensity::transform(Options& state) { +void EvolveDensity::transform_impl(GuardedOptions& state) { AUTO_TRACE(); if (evolve_log) { @@ -176,7 +190,7 @@ void EvolveDensity::transform(Options& state) { } } - auto& species = state["species"][name]; + auto species = state["species"][name]; set(species["density"], floor(N, 0.0)); // Density in state always >= 0 set(species["AA"], AA); // Atomic mass if (charge != 0.0) { // Don't set charge for neutral species diff --git a/src/evolve_energy.cxx b/src/evolve_energy.cxx index 6b55067b0..7f9edde77 100644 --- a/src/evolve_energy.cxx +++ b/src/evolve_energy.cxx @@ -17,7 +17,9 @@ using bout::globals::mesh; EvolveEnergy::EvolveEnergy(std::string name, Options& alloptions, Solver* solver) - : name(name) { + : Component( + {readOnly("species:{name}:{inputs}"), readWrite("species:{name}:{outputs}")}), + name(name) { AUTO_TRACE(); auto& options = alloptions[name]; @@ -111,9 +113,13 @@ EvolveEnergy::EvolveEnergy(std::string name, Options& alloptions, Solver* solver thermal_conduction = options["thermal_conduction"] .doc("Include parallel heat conduction?") .withDefault(true); + + substitutePermissions("name", {name}); + substitutePermissions("inputs", {"AA", "density", "velocity"}); + substitutePermissions("outputs", {"pressure", "temperature"}); } -void EvolveEnergy::transform(Options& state) { +void EvolveEnergy::transform_impl(GuardedOptions& state) { AUTO_TRACE(); if (evolve_log) { @@ -123,7 +129,7 @@ void EvolveEnergy::transform(Options& state) { mesh->communicate(E); - auto& species = state["species"][name]; + auto species = state["species"][name]; N = getNoBoundary(species["density"]); const Field3D V = getNoBoundary(species["velocity"]); const BoutReal AA = get(species["AA"]); diff --git a/src/evolve_momentum.cxx b/src/evolve_momentum.cxx index 3ccadf51a..76c1a3783 100644 --- a/src/evolve_momentum.cxx +++ b/src/evolve_momentum.cxx @@ -12,7 +12,11 @@ using bout::globals::mesh; -EvolveMomentum::EvolveMomentum(std::string name, Options &alloptions, Solver *solver) : name(name) { +EvolveMomentum::EvolveMomentum(std::string name, Options& alloptions, Solver* solver) + : Component({readOnly("species:{name}:AA"), + readOnly("species:{name}:density", Regions::Interior), + readWrite("species:{name}:{outputs}")}), + name(name) { AUTO_TRACE(); // Evolve the momentum in time @@ -58,13 +62,16 @@ EvolveMomentum::EvolveMomentum(std::string name, Options &alloptions, Solver *so // Set to zero so set for output momentum_source = 0.0; NV_err = 0.0; + + substitutePermissions("name", {name}); + substitutePermissions("outputs", {"velocity", "momentum"}); } -void EvolveMomentum::transform(Options &state) { +void EvolveMomentum::transform_impl(GuardedOptions& state) { AUTO_TRACE(); mesh->communicate(NV); - auto& species = state["species"][name]; + auto species = state["species"][name]; // Not using density boundary condition auto N = getNoBoundary(species["density"]); diff --git a/src/evolve_pressure.cxx b/src/evolve_pressure.cxx index 9b2ee1833..609aa2fd7 100644 --- a/src/evolve_pressure.cxx +++ b/src/evolve_pressure.cxx @@ -16,7 +16,9 @@ using bout::globals::mesh; EvolvePressure::EvolvePressure(std::string name, Options& alloptions, Solver* solver) - : name(name) { + : Component({readOnly("species:{name}:{inputs}", Regions::Interior), + readWrite("species:{name}:{outputs}")}), + name(name) { AUTO_TRACE(); auto& options = alloptions[name]; @@ -162,9 +164,16 @@ EvolvePressure::EvolvePressure(std::string name, Options& alloptions, Solver* so thermal_conduction = options["thermal_conduction"] .doc("Include parallel heat conduction?") .withDefault(true); + + if (source_time_dependent) { + setPermissions(readOnly("time")); + } + substitutePermissions("name", {name}); + substitutePermissions("inputs", {"density"}); + substitutePermissions("outputs", {"pressure", "temperature"}); } -void EvolvePressure::transform(Options& state) { +void EvolvePressure::transform_impl(GuardedOptions& state) { AUTO_TRACE(); if (evolve_log) { @@ -208,7 +217,7 @@ void EvolvePressure::transform(Options& state) { } } - auto& species = state["species"][name]; + auto species = state["species"][name]; // Calculate temperature // Not using density boundary condition diff --git a/src/fixed_fraction_ions.cxx b/src/fixed_fraction_ions.cxx index bc89357b1..69113d548 100644 --- a/src/fixed_fraction_ions.cxx +++ b/src/fixed_fraction_ions.cxx @@ -1,8 +1,9 @@ #include "../include/fixed_fraction_ions.hxx" -FixedFractionIons::FixedFractionIons(std::string name, Options &alloptions, - Solver *UNUSED(solver)) { +FixedFractionIons::FixedFractionIons(std::string name, Options& alloptions, + Solver* UNUSED(solver)) + : Component({readOnly("species:e:density"), readWrite("species:{sp}:density")}) { std::string fractions_str = alloptions[name]["fractions"] @@ -10,6 +11,8 @@ FixedFractionIons::FixedFractionIons(std::string name, Options &alloptions, "'species1@fraction1, species2@fraction2'") .as(); + std::vector specified_species; + for (const auto &pair : strsplit(fractions_str, ',')) { auto species_frac = strsplit(pair, '@'); if (species_frac.size() != 2) { @@ -20,15 +23,17 @@ FixedFractionIons::FixedFractionIons(std::string name, Options &alloptions, BoutReal fraction = stringToReal(trim(species_frac.back())); fractions.emplace_back(std::pair(species, fraction)); + specified_species.push_back(species); } // Check that there are some species if (fractions.size() == 0) { throw BoutException("No ion species specified. Got fractions = '%s'", fractions_str.c_str()); } + substitutePermissions("sp", specified_species); } -void FixedFractionIons::transform(Options &state) { +void FixedFractionIons::transform_impl(GuardedOptions& state) { AUTO_TRACE(); // Electron density diff --git a/src/guarded_options.cxx b/src/guarded_options.cxx new file mode 100644 index 000000000..ed4cd248a --- /dev/null +++ b/src/guarded_options.cxx @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../include/guarded_options.hxx" +#include "../include/permissions.hxx" + +/// Check whether an option is set, without creating any parent +/// Options objects in the process. +bool isSetRecursive(Options& opt, const std::string& varname) { + const size_t colon = varname.find(":"); + if (colon == std::string::npos) { + return opt.isSet(varname); + } + const std::string fragment = varname.substr(0, colon); + if (not opt.isSection(fragment)) { + return false; + } + return isSetRecursive(opt[fragment], varname.substr(colon + 1)); +} + +GuardedOptions::GuardedOptions(Options* options, Permissions* permissions) + : options(options), permissions(permissions), + unread_variables(std::make_shared>()), + unwritten_variables(std::make_shared>()) { + if (options == nullptr) { + throw BoutException("Can not construct GuardedOptions with null options pointer."); + } + if (permissions == nullptr) { + throw BoutException( + "Can not construct GuardedOptions with null permissions pointer."); + } +#if CHECKLEVEL >= 1 + *unread_variables = permissions->getVariablesWithPermission(PermissionTypes::Read); + // Only add variables with permission ReadIfSet to + // unread_variables if they are already present in the options + // object + for (auto& [varname, region] : + permissions->getVariablesWithPermission(PermissionTypes::ReadIfSet)) { + if (isSetRecursive(*options, varname)) { + unread_variables->insert({varname, region}); + } + } + *unwritten_variables = + permissions->getVariablesWithPermission(PermissionTypes::Write, false); +#endif +} + +std::map GuardedOptions::getChildren() { + std::map result; + for (const auto& [varname, _] : options->getChildren()) { + result.insert({varname, (*this)[varname]}); + } + return result; +} + +void updateAccessRecords(std::map& records, const std::string& name, + Regions region) { + if (records.count(name) > 0) { + const Regions new_region = records[name] & ~region; + if (new_region == Regions::Nowhere) { + records.erase(name); + } else { + records[name] = new_region; + } + } +} + +const Options& GuardedOptions::get([[maybe_unused]] Regions region) const { +#if CHECKLEVEL >= 1 + const std::string name = options->str(); + auto [permission, varname] = permissions->getHighestPermission(name, region); + if (permission >= PermissionTypes::ReadIfSet) { + if (permission == PermissionTypes::ReadIfSet && !options->isSet()) { + throw BoutException( + "Only have permission to read {} if it is already set, which it is not.", name); + } + updateAccessRecords(*unread_variables, varname, region); + return *options; + } + throw BoutException("Do not have read permission for {}.", name); +#else + return *options; +#endif +} + +Options& GuardedOptions::getWritable([[maybe_unused]] Regions region) { +#if CHECKLEVEL >= 1 + const std::string name = options->str(); + auto [access, varname] = permissions->canAccess(name, PermissionTypes::Write, region); + if (access) { + updateAccessRecords(*unwritten_variables, varname, region); + return *options; + } + throw BoutException("Do not have write permission for {}.", options->str()); +#else + return *options; +#endif +} + +bool GuardedOptions::operator==(const GuardedOptions& other) const { + return std::tie(options, permissions, unread_variables, unwritten_variables) + == std::tie(other.options, other.permissions, other.unread_variables, + other.unwritten_variables); +} + +bool GuardedOptions::operator!=(const GuardedOptions& other) const { + return !(*this == other); +} diff --git a/src/hydrogen_charge_exchange.cxx b/src/hydrogen_charge_exchange.cxx index cc9051558..44cca5f8d 100644 --- a/src/hydrogen_charge_exchange.cxx +++ b/src/hydrogen_charge_exchange.cxx @@ -1,7 +1,7 @@ #include "../include/hydrogen_charge_exchange.hxx" -void HydrogenChargeExchange::calculate_rates(Options& atom1, Options& ion1, - Options& atom2, Options& ion2, +void HydrogenChargeExchange::calculate_rates(GuardedOptions&& atom1, GuardedOptions&& ion1, + GuardedOptions&& atom2, GuardedOptions&& ion2, Field3D &R, Field3D &atom_mom, Field3D &ion_mom, Field3D &atom_energy, Field3D &ion_energy, @@ -49,7 +49,7 @@ void HydrogenChargeExchange::calculate_rates(Options& atom1, Options& ion1, R = Natom * Nion * sigmav; // Rate coefficient in [m^-3 s^-1] - if ((&atom1 != &atom2) or (&ion1 != &ion2)) { + if ((atom1 != atom2) or (ion1 != ion2)) { // Transfer particles atom1 -> ion2, ion1 -> atom2 subtract(atom1["density_source"], R); add(ion2["density_source"], R); @@ -103,6 +103,10 @@ void HydrogenChargeExchange::calculate_rates(Options& atom1, Options& ion1, add(ion1["collision_frequency"], ion_rate); // Set individual collision frequencies - set(atom1["collision_frequencies"][atom1.name() + std::string("_") + ion1.name() + std::string("_cx")], atom_rate); - set(ion1["collision_frequencies"][ion1.name() + std::string("_") + atom1.name() + std::string("_cx")], ion_rate); + set(atom1["collision_frequencies"] + [atom1.name() + std::string("_") + ion1.name() + std::string("_cx")], + atom_rate); + set(ion1["collision_frequencies"] + [ion1.name() + std::string("_") + atom1.name() + std::string("_cx")], + ion_rate); } diff --git a/src/ionisation.cxx b/src/ionisation.cxx index 1c779ae2b..848ece60e 100644 --- a/src/ionisation.cxx +++ b/src/ionisation.cxx @@ -21,7 +21,15 @@ BoutReal ionisation_rate(BoutReal T) { } } // namespace -Ionisation::Ionisation(std::string name, Options &alloptions, Solver *) { +Ionisation::Ionisation(std::string name, Options& alloptions, Solver*) + : Component( + {readOnly("species:h:density"), readOnly("species:h:temperature"), + readOnly("species:h:velocity"), readOnly("species:h:AA"), + readOnly("species:e:density"), readOnly("species:e:temperature"), + readOnly("species:h+:AA"), readWrite("species:h:density_source"), + readWrite("species:h+:density_source"), readWrite("species:h:momentum_source"), + readWrite("species:h+:momentum_source"), readWrite("species:h:energy_source"), + readWrite("species:h+:energy_source"), readWrite("species:e:energy_source")}) { // Get options for this component auto& options = alloptions[name]; @@ -38,19 +46,19 @@ Ionisation::Ionisation(std::string name, Options &alloptions, Solver *) { Eionize /= Tnorm; } -void Ionisation::transform(Options &state) { +void Ionisation::transform_impl(GuardedOptions& state) { // Get neutral atom properties - Options& hydrogen = state["species"]["h"]; + GuardedOptions hydrogen = state["species"]["h"]; Field3D Nn = get(hydrogen["density"]); Field3D Tn = get(hydrogen["temperature"]); Field3D Vn = get(hydrogen["velocity"]); auto AA = get(hydrogen["AA"]); - Options& electron = state["species"]["e"]; + GuardedOptions electron = state["species"]["e"]; Field3D Ne = get(electron["density"]); Field3D Te = get(electron["temperature"]); - Options& ion = state["species"]["h+"]; + GuardedOptions ion = state["species"]["h+"]; ASSERT1(AA == get(ion["AA"])); Field3D reaction_rate = cellAverage( diff --git a/src/isothermal.cxx b/src/isothermal.cxx index eaca1b1ee..186bbed72 100644 --- a/src/isothermal.cxx +++ b/src/isothermal.cxx @@ -3,9 +3,12 @@ #include "../include/isothermal.hxx" -Isothermal::Isothermal(std::string name, Options &alloptions, - Solver *UNUSED(solver)) - : name(name) { +Isothermal::Isothermal(std::string name, Options& alloptions, Solver* UNUSED(solver)) + : Component({readIfSet(fmt::format("species:{}:density", name), Regions::Interior), + readWrite(fmt::format("species:{}:temperature", name)), + // FIXME: This is only written if density is set + readWrite(fmt::format("species:{}:pressure", name))}), + name(name) { AUTO_TRACE(); Options& options = alloptions[name]; @@ -18,10 +21,10 @@ Isothermal::Isothermal(std::string name, Options &alloptions, .withDefault(false); } -void Isothermal::transform(Options &state) { +void Isothermal::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& species = state["species"][name]; + GuardedOptions species = state["species"][name]; set(species["temperature"], T); diff --git a/src/neutral_boundary.cxx b/src/neutral_boundary.cxx index 4879eef99..53f48d760 100644 --- a/src/neutral_boundary.cxx +++ b/src/neutral_boundary.cxx @@ -7,7 +7,10 @@ using bout::globals::mesh; NeutralBoundary::NeutralBoundary(std::string name, Options& alloptions, [[maybe_unused]] Solver* solver) - : name(name) { + : Component({writeBoundaryFinal("species:{name}:{outputs}"), + writeBoundaryFinalIfSet("species:{name}:{conditional_outputs}"), + readWrite("species:{name}:energy_source")}), + name(name) { AUTO_TRACE(); auto& options = alloptions[name]; @@ -49,11 +52,15 @@ NeutralBoundary::NeutralBoundary(std::string name, Options& alloptions, options["pfr_fast_refl_fraction"] .doc("Fraction of neutrals that are undergoing fast reflection at the pfr") .withDefault(0.8); + + substitutePermissions("name", {name}); + substitutePermissions("outputs", {"density", "temperature", "pressure"}); + substitutePermissions("conditional_outputs", {"velocity", "momentum"}); } -void NeutralBoundary::transform(Options& state) { +void NeutralBoundary::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - auto& species = state["species"][name]; + auto species = state["species"][name]; const BoutReal AA = get(species["AA"]); Field3D Nn = toFieldAligned(GET_NOBOUNDARY(Field3D, species["density"])); diff --git a/src/neutral_full_velocity.cxx b/src/neutral_full_velocity.cxx index 0adbf9387..255328316 100644 --- a/src/neutral_full_velocity.cxx +++ b/src/neutral_full_velocity.cxx @@ -14,7 +14,7 @@ using bout::globals::mesh; NeutralFullVelocity::NeutralFullVelocity(const std::string& name, Options& alloptions, Solver* solver) - : name(name) { + : Component({readWrite("species:{name}:{outputs}")}), name(name) { AUTO_TRACE(); // This is used in both transform and finally functions @@ -188,10 +188,13 @@ NeutralFullVelocity::NeutralFullVelocity(const std::string& name, Options& allop // Ensure that guard cells are filled and consistent between processors mesh->communicate(Urx, Ury, Uzx, Uzy); mesh->communicate(Txr, Txz, Tyr, Tyz); + substitutePermissions("name", {name}); + substitutePermissions( + "outputs", {"AA", "density", "pressure", "temperature", "momentum", "velocity"}); } /// Modify the given simulation state -void NeutralFullVelocity::transform(Options& state) { +void NeutralFullVelocity::transform_impl(GuardedOptions& state) { AUTO_TRACE(); mesh->communicate(Nn2D, Vn2D, Pn2D); @@ -254,7 +257,7 @@ void NeutralFullVelocity::transform(Options& state) { Vnpar = Vn2D.y / (coord->J * coord->Bxy); // Set values in the state - auto& localstate = state["species"][name]; + auto localstate = state["species"][name]; set(localstate["density"], Nn2D); set(localstate["AA"], AA); // Atomic mass set(localstate["pressure"], Pn2D); diff --git a/src/neutral_mixed.cxx b/src/neutral_mixed.cxx index 5aa0c6525..4b3cda8ea 100644 --- a/src/neutral_mixed.cxx +++ b/src/neutral_mixed.cxx @@ -18,7 +18,7 @@ using bout::globals::mesh; using ParLimiter = hermes::Limiter; NeutralMixed::NeutralMixed(const std::string& name, Options& alloptions, Solver* solver) - : name(name) { + : Component({readWrite("species:{name}:{outputs}")}), name(name) { AUTO_TRACE(); // Normalisations @@ -169,9 +169,13 @@ NeutralMixed::NeutralMixed(const std::string& name, Options& alloptions, Solver* DnnNn.setBoundary(std::string("Dnn") + name); DnnPn.setBoundary(std::string("Dnn") + name); DnnNVn.setBoundary(std::string("Dnn") + name); + + substitutePermissions("name", {name}); + substitutePermissions( + "outputs", {"AA", "density", "pressure", "temperature", "momentum", "velocity"}); } -void NeutralMixed::transform(Options& state) { +void NeutralMixed::transform_impl(GuardedOptions& state) { AUTO_TRACE(); mesh->communicate(Nn, Pn, NVn); @@ -254,7 +258,7 @@ void NeutralMixed::transform(Options& state) { } // Set values in the state - auto& localstate = state["species"][name]; + auto localstate = state["species"][name]; set(localstate["density"], Nn); set(localstate["AA"], AA); // Atomic mass set(localstate["pressure"], Pn); diff --git a/src/neutral_parallel_diffusion.cxx b/src/neutral_parallel_diffusion.cxx index d8ff767f3..b66178b93 100644 --- a/src/neutral_parallel_diffusion.cxx +++ b/src/neutral_parallel_diffusion.cxx @@ -6,14 +6,14 @@ using bout::globals::mesh; -void NeutralParallelDiffusion::transform(Options& state) { +void NeutralParallelDiffusion::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (auto& kv : allspecies.getChildren()) { const auto& species_name = kv.first; // Get non-const reference - auto& species = allspecies[species_name]; + auto species = allspecies[species_name]; if (species.isSet("charge") and (get(species["charge"]) != 0.0)) { // Skip charged species @@ -38,14 +38,14 @@ void NeutralParallelDiffusion::transform(Options& state) { if (diffusion_collisions_mode == "afn") { for (const auto& collision : species["collision_frequencies"].getChildren()) { - std::string collision_name = collision.second.name(); + std::string collision_name = collision.first; if (// Charge exchange (collisionSpeciesMatch( - collision_name, species.name(), "+", "cx", "partial")) or + collision_name, species_name, "+", "cx", "partial")) or // Ionisation (collisionSpeciesMatch( - collision_name, species.name(), "+", "iz", "partial"))) { + collision_name, species_name, "+", "iz", "partial"))) { collision_names[species_name].push_back(collision_name); } @@ -54,30 +54,30 @@ void NeutralParallelDiffusion::transform(Options& state) { } else if (diffusion_collisions_mode == "multispecies") { for (const auto& collision : species["collision_frequencies"].getChildren()) { - std::string collision_name = collision.second.name(); + std::string collision_name = collision.first; if (// Charge exchange (collisionSpeciesMatch( - collision_name, species.name(), "", "cx", "partial")) or + collision_name, species_name, "", "cx", "partial")) or // Any collision (ne, ni, nn) (collisionSpeciesMatch( - collision_name, species.name(), "", "coll", "partial"))) { + collision_name, species_name, "", "coll", "partial"))) { collision_names[species_name].push_back(collision_name); } } } else { - throw BoutException("\tdiffusion_collisions_mode for {:s} must be either multispecies or afn", species.name()); + throw BoutException("\tdiffusion_collisions_mode for {:s} must be either multispecies or afn", species_name); } if (collision_names[species_name].empty()) { - throw BoutException("\tNo collisions found for {:s} in neutral_parallel_diffusion for selected collisions mode", species.name()); + throw BoutException("\tNo collisions found for {:s} in neutral_parallel_diffusion for selected collisions mode", species_name); } // Write chosen collisions to log file output_info.write("\t{:s} neutral diffusion collisionality mode: '{:s}' using ", - species.name(), diffusion_collisions_mode); + species_name, diffusion_collisions_mode); for (const auto& collision : collision_names[species_name]) { output_info.write("{:s} ", collision); } diff --git a/src/noflow_boundary.cxx b/src/noflow_boundary.cxx index bc9bf252a..d4bf09ead 100644 --- a/src/noflow_boundary.cxx +++ b/src/noflow_boundary.cxx @@ -11,13 +11,13 @@ Ind3D indexAt(const Field3D& f, int x, int y, int z) { } } -void NoFlowBoundary::transform(Options& state) { +void NoFlowBoundary::transform_impl(GuardedOptions& state) { AUTO_TRACE(); // Make sure that the state has been set for this species ASSERT1(state["species"].isSection(name)); - Options& species = state["species"][name]; + GuardedOptions species = state["species"][name]; // Apply zero-gradient boundary conditions to state variables for (const std::string field : {"density", "temperature", "pressure"}) { diff --git a/src/permissions.cxx b/src/permissions.cxx new file mode 100644 index 000000000..3fdc08482 --- /dev/null +++ b/src/permissions.cxx @@ -0,0 +1,250 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "../include/permissions.hxx" + +// TODO: It might be useful to add an optional condition which must be +// met for conditions to apply. So a variable is only read or written +// if another variable is already set, for example. Could potentially +// have conditions based on values as well. Could put in boolean +// logic. This would allow finer-grained access control in Components, +// but is it worth the hassle? +// +// struct Condition { +// virtual bool eval() const = 0; +// } +// +// struct IsSetCondition : Condition { +// std::string varname +// } +// +// struct TrueCondition; +// struct AndCondition; +// struct OrCondition; +// +// There should be ways to do this with templates to allow greater inlining +// + +const std::regex Permissions::LABEL_RE{"\\{([^}]+)\\}"}; + +/// Return a set of access rights where the lower permissions have +/// been updated so that they reflect higher permissions (e.g., read +/// permission will be set in all cases where write permission was +/// set). +Permissions::AccessRights applyLowerPermissions(const Permissions::AccessRights& rights) { + Permissions::AccessRights result(rights); + for (auto i = static_cast(PermissionTypes::ReadIfSet); + i < static_cast(PermissionTypes::END); i++) { + // Higher permissions imply lower permissions + for (auto j = static_cast(PermissionTypes::ReadIfSet); j < i; j++) { + result[j] = result[j] | rights[i]; + } + } + return result; +} + +const std::map Permissions::fundamental_regions = { + {Regions::Interior, "Interior"}, {Regions::Boundaries, "Boundaries"}}; + +Permissions::Permissions(std::initializer_list data) : variable_permissions() { + for (const auto& [varname, access] : data) { + setAccess(varname, access); + } +} + +void Permissions::setAccess(const std::string& variable, const AccessRights& rights) { + + variable_permissions[variable] = applyLowerPermissions(rights); +} + +std::string replaceAll(const std::string& str, const std::string& from, + const std::string& to) { + std::string result = str; + if (from.empty()) { + return result; + } + size_t start_pos = 0; + while ((start_pos = result.find(from, start_pos)) != std::string::npos) { + result.replace(start_pos, from.length(), to); + start_pos += + to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' + } + return result; +} + +void Permissions::substitute(const std::string& label, + const std::vector& substitutions) { + for (auto it = variable_permissions.begin(); it != variable_permissions.end();) { + const auto [varname, access] = *it; + const std::string pattern = "{" + label + "}"; + if (varname.find(pattern) == std::string::npos) { + it++; + continue; + } + it = variable_permissions.erase(it); + for (const std::string& val : substitutions) { + const std::string newname = replaceAll(varname, pattern, val); + // Do not overwrite permissions that are already set + if (variable_permissions.count(newname) == 0) { + variable_permissions[newname] = access; + } + } + } +} + +void Permissions::checkNoRemainingSubstitutions() const { + std::vector unsubstituted; + for (const auto& [varname, _] : variable_permissions) { + if (std::regex_search(varname, LABEL_RE)) { + unsubstituted.push_back(varname); + } + } + if (unsubstituted.size() > 0) { + throw BoutException("The following variable names have unsubstituted labels: {}", + fmt::join(unsubstituted, ", ")); + } +} + +Permissions::VarRights Permissions::bestMatchRights(const std::string& variable) const { + auto match = variable_permissions.find(variable); + if (match != variable_permissions.end()) { + return {match->first, match->second}; + } + Permissions::AccessRights best_candidate = {Regions::Nowhere, Regions::Nowhere, + Regions::Nowhere}; + std::string best_candidate_name = ""; + size_t max_len = 0; + for (const auto& [varname, rights] : variable_permissions) { + if (varname.size() > max_len and variable.find(varname + ":") == 0) { + max_len = varname.size(); + best_candidate = rights; + best_candidate_name = varname; + } + } + return {best_candidate_name, best_candidate}; +} + +std::pair Permissions::canAccess(const std::string& variable, + PermissionTypes permission, + Regions region) const { + auto [match_name, match_rights] = bestMatchRights(variable); + if ((match_rights[static_cast(permission)] & region) == region) { + return {true, match_name}; + } + return {false, ""}; +} + +std::pair +Permissions::getHighestPermission(const std::string& variable, Regions region) const { + if (region == Regions::Nowhere) { + return {PermissionTypes::None, ""}; + } + auto [varname, rights] = bestMatchRights(variable); + size_t i = static_cast(PermissionTypes::ReadIfSet); + while (i < static_cast(PermissionTypes::END) and (rights[i] & region) == region) { + i++; + } + return {static_cast(i - 1), varname}; +} + +std::map +Permissions::getVariablesWithPermission(PermissionTypes permission, + bool highestOnly) const { + if (permission == PermissionTypes::None) { + throw BoutException("Can not return information on variables with no permission."); + } + std::map result; + if (highestOnly + and static_cast(permission) < static_cast(PermissionTypes::END) - 1) { + for (const auto& [varname, rights] : variable_permissions) { + auto perm_in_regions = rights[static_cast(permission)] + & ~rights[static_cast(permission) + 1]; + if (perm_in_regions != Regions::Nowhere) { + result.emplace(varname, perm_in_regions); + } + } + } else { + for (const auto& [varname, rights] : variable_permissions) { + auto regions = rights[static_cast(permission)]; + if (regions != Regions::Nowhere) { + result.emplace(varname, regions); + } + } + } + return result; +} + +std::string Permissions::regionNames(const Regions regions) { + std::vector regions_present; + for (auto & [region, name] : fundamental_regions) { + if ((regions & region) == region) { + regions_present.push_back(name); + } + } + return fmt::format("{}", fmt::join(regions_present, ", ")); +} + +auto fmt::formatter::format(const Permissions& p, format_context& ctx) const + -> format_context::iterator { + return formatter>::format( + p.variable_permissions, ctx); +} + +std::ostream& operator<<(std::ostream& os, const Permissions& permissions) { + os << fmt::format("{}", permissions); + return os; +} + +std::istream& operator>>(std::istream& is, Permissions& permissions) { + std::string tmp; + std::string object; + std::getline(is, tmp, '{'); + if (is.eof()) { + throw BoutException("Error parsing Permissions data; no opening bracket."); + } + std::getline(is, object, '}'); + if (is.eof()) { + throw BoutException("Error parsing Permissions data; no closing bracket."); + } + permissions.variable_permissions.clear(); + + // FIXME: This will just skip over any malformed content, without an error or warning + std::vector rights_patterns; + for (int i = 0; i < static_cast(PermissionTypes::END); i++) { + rights_patterns.push_back("\\s*(\\d+)\\s*"); + } + const std::regex re( + fmt::format("\\s*\"([^\"]+)\"\\s*:\\s*\\[{}\\]", fmt::join(rights_patterns, ","))); + auto items_begin = std::sregex_iterator(object.begin(), object.end(), re); + auto items_end = std::sregex_iterator(); + + for (std::sregex_iterator i = items_begin; i != items_end; ++i) { + const std::smatch item = *i; + Permissions::AccessRights rights; + for (size_t i = 0; i < static_cast(PermissionTypes::END); i++) { + const std::string val = item[2 + i]; + rights[i] = static_cast(std::stoi(val)); + } + permissions.setAccess(item[1], rights); + } + + return is; +} + +std::string toString(const Permissions& value) { + std::ostringstream ss; + ss << value; + return ss.str(); +} diff --git a/src/polarisation_drift.cxx b/src/polarisation_drift.cxx index 5c49b4509..18e639392 100644 --- a/src/polarisation_drift.cxx +++ b/src/polarisation_drift.cxx @@ -8,9 +8,15 @@ using bout::globals::mesh; -PolarisationDrift::PolarisationDrift(std::string name, - Options &alloptions, - Solver *UNUSED(solver)) { +PolarisationDrift::PolarisationDrift(std::string name, Options& alloptions, + Solver* UNUSED(solver)) + // FIXME: There is a lot of complicated conditional logic which is not being captured + // here. E.g., species without AA or momentum will be skipped. + : Component({readIfSet("species:{all_species}:charge"), + readOnly("species:{charged}:{inputs}"), + readIfSet("species:{charged}:{optional_inputs}"), + readIfSet("fields:{fields}"), + readWrite("species:{charged}:{outputs}")}) { AUTO_TRACE(); // Get options for this component @@ -57,13 +63,31 @@ PolarisationDrift::PolarisationDrift(std::string name, diagnose = options["diagnose"] .doc("Output additional diagnostics?") .withDefault(false); + + // Pressure interior only, + std::vector inputs = {"AA"}, fields = {"DivJdia"}, + outputs = {"energy_source"}; + if (advection) { + // Interior only + inputs.push_back("density"); + fields.push_back("DivJextra"); + fields.push_back("DivJdia"); + outputs.push_back("density_source"); + outputs.push_back("momentum_source"); + } + substitutePermissions("inputs", {"AA", "density"}); + substitutePermissions("optional_inputs", {"momentum, pressure"}); + substitutePermissions("fields", fields); + // FIXME: energy_source and momentum source are only set if pressure + // and momentum were set, respectively + substitutePermissions("outputs", outputs); } -void PolarisationDrift::transform(Options &state) { +void PolarisationDrift::transform_impl(GuardedOptions& state) { AUTO_TRACE(); // Iterate through all subsections - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; // Calculate divergence of all currents except the polarisation current @@ -75,7 +99,7 @@ void PolarisationDrift::transform(Options &state) { // Parallel current due to species parallel flow for (auto& kv : allspecies.getChildren()) { - const Options& species = kv.second; + const GuardedOptions species = kv.second; if (!species.isSet("charge") or !species.isSet("momentum")) { continue; // Not charged, or no parallel flow @@ -103,7 +127,7 @@ void PolarisationDrift::transform(Options &state) { // Calculate energy exchange term nonlinear in pressure // (3 / 2) ddt(Pi) += (Pi / n0) * Div((Pe + Pi) * Curlb_B + Jpar); for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: need non-const + GuardedOptions species = allspecies[kv.first]; // Note: need non-const if (!(IS_SET_NOBOUNDARY(species["pressure"]) and species.isSet("charge") and species.isSet("AA"))) { @@ -139,7 +163,7 @@ void PolarisationDrift::transform(Options &state) { } else { mass_density = 0.0; for (auto& kv : allspecies.getChildren()) { - const Options& species = kv.second; + const GuardedOptions species = kv.second; if (!(species.isSet("charge") and species.isSet("AA"))) { continue; // No charge or mass -> no current @@ -185,7 +209,7 @@ void PolarisationDrift::transform(Options &state) { // v_p = - (m_i / (Z_i * B^2)) * Grad(phi_pol) // for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: need non-const + GuardedOptions species = allspecies[kv.first]; // Note: need non-const if (!(species.isSet("charge") and species.isSet("AA"))) { continue; // No charge or mass -> no current diff --git a/src/quasineutral.cxx b/src/quasineutral.cxx index 77edd6850..53f615730 100644 --- a/src/quasineutral.cxx +++ b/src/quasineutral.cxx @@ -3,9 +3,12 @@ #include "../include/quasineutral.hxx" -Quasineutral::Quasineutral(std::string name, Options &alloptions, - Solver *UNUSED(solver)) - : name(name) { +Quasineutral::Quasineutral(std::string name, Options& alloptions, Solver* UNUSED(solver)) + : Component({readWrite("species:{name}:{outputs}"), + // FIXME: These are only read if BOTH are set + readIfSet("species:{all_species}:charge"), + readIfSet("species:{all_species}:density", Regions::Interior)}), + name(name) { Options &options = alloptions[name]; // Need to have a charge and mass @@ -13,22 +16,25 @@ Quasineutral::Quasineutral(std::string name, Options &alloptions, AA = options["AA"].doc("Particle atomic mass. Proton = 1"); ASSERT0(charge != 0.0); + substitutePermissions("name", {name}); + substitutePermissions("outputs", {"AA", "charge", "density"}); } -void Quasineutral::transform(Options &state) { +void Quasineutral::transform_impl(GuardedOptions& state) { AUTO_TRACE(); // Iterate through all subsections - Options &allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; + std::map children = allspecies.getChildren(); // Add charge density of other species const Field3D rho = std::accumulate( // Iterate through species - begin(allspecies.getChildren()), end(allspecies.getChildren()), + begin(children), end(children), // Start with no charge Field3D(0.0), [this](Field3D value, - const std::map::value_type &name_species) { - const Options &species = name_species.second; + const std::map::value_type& name_species) { + const GuardedOptions species = name_species.second; // Add other species which have density and charge if (name_species.first != name and species.isSet("charge") and species.isSet("density")) { @@ -40,7 +46,7 @@ void Quasineutral::transform(Options &state) { }); // Set quantites for this species - Options &species = allspecies[name]; + GuardedOptions species = allspecies[name]; // Calculate density required. Floor so that density is >= 0 density = floor(rho / (-charge), 0.0); diff --git a/src/reaction.cxx b/src/reaction.cxx index 595b120e3..020753dd8 100644 --- a/src/reaction.cxx +++ b/src/reaction.cxx @@ -9,7 +9,10 @@ #include "integrate.hxx" -Reaction::Reaction(std::string name, Options& options) : name(name) { +Reaction::Reaction(std::string name, Options& options, const std::string & reaction_str) + : ReactionBase({readOnly("species:{sp}:{r_val}"), readOnly("species:e:{e_val}"), + readWrite("species:{sp}:{w_val}")}), + name(name) { // Extract some relevant options, units to member vars for readability const auto& units = options["units"]; @@ -21,28 +24,20 @@ Reaction::Reaction(std::string name, Options& options) : name(name) { .doc("Output additional diagnostics?") .withDefault(false); - /* - * Awful hack to extract the correct reaction expression from the params; depends on - * instantiation order matching the order reactions are listed in the input file. There - * must be a better way... - */ - std::string reaction_grp_str = options[name]["type"]; - std::regex match_parentheses("\\(|\\)"); - reaction_grp_str = std::regex_replace(reaction_grp_str, match_parentheses, ""); - std::string reaction_str; - std::stringstream ss(reaction_grp_str); - for (auto ii = 0; ii < this->inst_num; ii++) { - std::getline(ss, reaction_str, ','); - } - // Parse the reaction string this->parser = std::make_unique(reaction_str); + std::vector species = this->parser->get_species(); // Participation factors. All set to unity for now; could make configurable in future. - for (const std::string& sp : this->parser->get_species()) { + for (const std::string& sp : species) { this->pfactors[sp] = 1; } + substitutePermissions("sp", species); + substitutePermissions("r_val", {"AA", "density", "velocity", "temperature"}); + substitutePermissions("e_val", {"density", "temperature"}); + substitutePermissions("w_val", {"momentum_source", "energy_source", "density_source"}); + // Initialise weight sums with dummy values. Real values are set on first call to // transform(). this->momentum_weightsum = -1; @@ -76,6 +71,7 @@ void Reaction::add_diagnostic(const std::string& sp_name, const std::string& dia diag_key, ReactionDiagnostic(diag_name, description, type, data_source, standard_name, transformer))); } + setPermissions(readWrite(diag_name)); } /** @@ -85,7 +81,7 @@ void Reaction::add_diagnostic(const std::string& sp_name, const std::string& dia * * @param state current simulation state */ -void Reaction::calc_weightsums(Options& state) { +void Reaction::calc_weightsums(GuardedOptions & state) { if (this->energy_weightsum < 0 || this->momentum_weightsum < 0) { this->momentum_weightsum = 0; this->energy_weightsum = 0; @@ -122,7 +118,7 @@ void Reaction::outputVars(Options& state) { * * @param state */ -void Reaction::transform(Options& state) { +void Reaction::transform_impl(GuardedOptions& state) { Field3D momentum_exchange, energy_exchange, energy_loss; zero_diagnostics(state); @@ -131,7 +127,7 @@ void Reaction::transform(Options& state) { parser->get_species(species_filter::reactants); // Extract electron properties - Options& electron = state["species"]["e"]; + GuardedOptions electron = state["species"]["e"]; Field3D n_e = get(electron["density"]); Field3D T_e = get(electron["temperature"]); @@ -228,7 +224,7 @@ void Reaction::transform(Options& state) { * * @param state */ -void Reaction::zero_diagnostics(Options& state) { +void Reaction::zero_diagnostics(GuardedOptions& state) { if (this->diagnose) { for (auto& [key, diag] : diagnostics) { set(state[diag.name], 0.0); diff --git a/src/recycling.cxx b/src/recycling.cxx index 93688ec5b..a55403f7b 100644 --- a/src/recycling.cxx +++ b/src/recycling.cxx @@ -11,7 +11,10 @@ using bout::globals::mesh; -Recycling::Recycling(std::string name, Options& alloptions, Solver*) { +Recycling::Recycling(std::string name, Options& alloptions, Solver*) + : Component({readOnly("species:{from}:{from_inputs}"), + readOnly("species:{to}:{to_inputs}"), + readWrite("species:{to}:{outputs}")}) { AUTO_TRACE(); const Options& units = alloptions["units"]; @@ -24,7 +27,8 @@ Recycling::Recycling(std::string name, Options& alloptions, Solver*) { .as(), ','); - + std::set from_species, to_species; + // Neutral pump // Mark cells as having a pump by setting the Field2D is_pump to 1 in the grid file // Works only on SOL and PFR edges, where it locally modifies the recycle multiplier to the pump albedo @@ -43,6 +47,9 @@ Recycling::Recycling(std::string name, Options& alloptions, Solver*) { .doc("Name of the species to recycle into") .as(); + from_species.insert(from); + to_species.insert(to); + density_floor = options["density_floor"].doc("Minimum density floor").withDefault(1e-7); pressure_floor = density_floor * (1./get(alloptions["units"]["eV"])); @@ -128,6 +135,8 @@ Recycling::Recycling(std::string name, Options& alloptions, Solver*) { target_recycle_energy, sol_recycle_energy, pfr_recycle_energy, target_fast_recycle_fraction, pfr_fast_recycle_fraction, sol_fast_recycle_fraction, target_fast_recycle_energy_factor, sol_fast_recycle_energy_factor, pfr_fast_recycle_energy_factor}); + // FIXME: These are global settings, but are being overwritten by each particular + // recycling channel // Boolean flags for enabling recycling in different regions target_recycle = from_options["target_recycle"] @@ -146,9 +155,24 @@ Recycling::Recycling(std::string name, Options& alloptions, Solver*) { .doc("Neutral pump enabled? Note, need location in grid file") .withDefault(false); } + + if (target_recycle) { + setPermissions(readIfSet("species:{from}:energy_flow_ylow")); + } + if (sol_recycle or pfr_recycle) { + setPermissions(readIfSet("species:{from}:energy_flow_xlow")); + setPermissions(readIfSet("species:{from}:particle_flow_xlow")); + } + substitutePermissions("to", + std::vector(to_species.begin(), to_species.end())); + substitutePermissions( + "from", std::vector(from_species.begin(), from_species.end())); + substitutePermissions("to_inputs", {"AA", "density", "pressure", "temperature"}); + substitutePermissions("from_inputs", {"density", "velocity", "temperature"}); + substitutePermissions("outputs", {"density_source", "energy_source"}); } -void Recycling::transform(Options& state) { +void Recycling::transform_impl(GuardedOptions& state) { AUTO_TRACE(); // Get metric tensor components @@ -160,13 +184,13 @@ void Recycling::transform(Options& state) { const Field2D& g_22 = coord->g_22; for (auto& channel : channels) { - const Options& species_from = state["species"][channel.from]; + const GuardedOptions species_from = state["species"][channel.from]; const Field3D N = get(species_from["density"]); const Field3D V = get(species_from["velocity"]); // Parallel flow velocity const Field3D T = get(species_from["temperature"]); // Ion temperature - Options& species_to = state["species"][channel.to]; + GuardedOptions species_to = state["species"][channel.to]; const Field3D Nn = get(species_to["density"]); const Field3D Pn = get(species_to["pressure"]); const Field3D Tn = get(species_to["temperature"]); diff --git a/src/relax_potential.cxx b/src/relax_potential.cxx index aa7bbb061..539e77e55 100644 --- a/src/relax_potential.cxx +++ b/src/relax_potential.cxx @@ -6,7 +6,8 @@ using bout::globals::mesh; #include "../include/div_ops.hxx" #include "../include/relax_potential.hxx" -RelaxPotential::RelaxPotential(std::string name, Options& alloptions, Solver* solver) { +RelaxPotential::RelaxPotential(std::string name, Options& alloptions, Solver* solver) + : Component({readWrite("fields:vorticity"), readWrite("fields:phi")}) { AUTO_TRACE(); auto* coord = mesh->getCoordinates(); @@ -44,6 +45,16 @@ RelaxPotential::RelaxPotential(std::string name, Options& alloptions, Solver* so solver->add(phi1, "phi1"); // Evolving scaled potential ϕ_1 = λ_2 ϕ if (diamagnetic) { + // FIXME: These will only be read if BOTH charge and pressure are set + setPermissions(readIfSet("species:{charged}:pressure", Regions::Interior)); + setPermissions(readIfSet("species:{all_species}:charge")); + // FIXME: The weay transform_impl is currently written, + // energy_source is set for neutral species with an explicit + // charge declared as 0 if diamagnetic_polarisation == true. I + // suspect that's a mistake though. + setPermissions(readWrite("species:{all_species}:energy_source")); + setPermissions(readWrite("fields:DivJdia")); + // Read curvature vector try { Curlb_B.covariant = false; // Contravariant @@ -78,7 +89,7 @@ RelaxPotential::RelaxPotential(std::string name, Options& alloptions, Solver* so Bsq = SQ(coord->Bxy); } -void RelaxPotential::transform(Options& state) { +void RelaxPotential::transform_impl(GuardedOptions& state) { AUTO_TRACE(); // Scale potential @@ -86,7 +97,7 @@ void RelaxPotential::transform(Options& state) { phi.applyBoundary("neumann"); Vort.applyBoundary("neumann"); - auto& fields = state["fields"]; + auto fields = state["fields"]; ddt(Vort) = 0.0; @@ -100,13 +111,13 @@ void RelaxPotential::transform(Options& state) { Jdia.z = 0.0; Jdia.covariant = Curlb_B.covariant; - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; // Pre-calculate this rather than calculate for each species Vector3D Grad_phi = Grad(phi); for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: need non-const + GuardedOptions species = allspecies[kv.first]; // Note: need non-const if (!(IS_SET_NOBOUNDARY(species["pressure"]) and IS_SET(species["charge"]) and (get(species["charge"]) != 0.0))) { @@ -135,8 +146,9 @@ void RelaxPotential::transform(Options& state) { // Calculate energy exchange term nonlinear in pressure // ddt(Pi) += Pi * Div((Pe + Pi) * Curlb_B); for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: need non-const + GuardedOptions species = allspecies[kv.first]; // Note: need non-const + // FIXME: Should this apply even if charge is 0? if (!(IS_SET_NOBOUNDARY(species["pressure"]) and IS_SET(species["charge"]) and IS_SET(species["AA"]))) { continue; // No pressure, charge or mass -> no polarisation current due to diff --git a/src/sheath_boundary.cxx b/src/sheath_boundary.cxx index b574f44cc..d0d07108a 100644 --- a/src/sheath_boundary.cxx +++ b/src/sheath_boundary.cxx @@ -45,7 +45,21 @@ BoutReal limitFree(BoutReal fm, BoutReal fc) { } // namespace -SheathBoundary::SheathBoundary(std::string name, Options& alloptions, Solver*) { +SheathBoundary::SheathBoundary(std::string name, Options& alloptions, Solver*) + : Component({ + readIfSet("species:{all_species}:charge"), + readIfSet("species:e:{e_whole_domain}"), + writeBoundaryFinal("species:e:{e_boundary}"), + readWrite("species:e:energy_source"), + writeBoundaryFinalIfSet("species:e:{e_optional}"), + writeBoundaryReadInteriorIfSet("species:e:pressure"), + readIfSet("species:{ions}:{ion_whole_domain}"), + readOnly("species:{ions}:AA"), + readWrite("species:{ions}:energy_source"), + writeBoundaryFinal("species:{ions}:{ion_boundary}"), + writeBoundaryReadInteriorIfSet("species:{ions}:pressure"), + writeBoundaryFinalIfSet("species:{ions}:{ion_optional}"), + }) { AUTO_TRACE(); Options& options = alloptions[name]; @@ -94,13 +108,22 @@ SheathBoundary::SheathBoundary(std::string name, Options& alloptions, Solver*) { floor_potential = options["floor_potential"] .doc("Apply a floor to wall potential when calculating Ve?") .withDefault(true); + + substitutePermissions("e_whole_domain", {"AA", "adiabatic"}); + substitutePermissions("e_boundary", {"density", "temperature"}); + substitutePermissions("e_optional", {"velocity", "momentum"}); + substitutePermissions("ion_whole_domain", {"charge", "adiabatic"}); + substitutePermissions("ion_boundary", {"density", "temperature"}); + substitutePermissions("ion_optional", {"velocity", "momentum"}); + setPermissions(always_set_phi ? writeBoundaryReadInteriorIfSet("fields:phi") + : writeBoundaryIfSet("fields:phi")); } -void SheathBoundary::transform(Options& state) { +void SheathBoundary::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; - Options& electrons = allspecies["e"]; + GuardedOptions allspecies = state["species"]; + GuardedOptions electrons = allspecies["e"]; // Need electron properties // Not const because boundary conditions will be set @@ -149,7 +172,7 @@ void SheathBoundary::transform(Options& state) { // Iterate through charged ion species for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; + GuardedOptions species = allspecies[kv.first]; if ((kv.first == "e") or !IS_SET(species["charge"]) or (get(species["charge"]) == 0.0)) { @@ -464,7 +487,7 @@ void SheathBoundary::transform(Options& state) { continue; // Skip electrons } - Options& species = allspecies[kv.first]; // Note: Need non-const + GuardedOptions species = allspecies[kv.first]; // Note: Need non-const // Ion charge const BoutReal Zi = diff --git a/src/sheath_boundary_insulating.cxx b/src/sheath_boundary_insulating.cxx index a501639f6..17098e3da 100644 --- a/src/sheath_boundary_insulating.cxx +++ b/src/sheath_boundary_insulating.cxx @@ -49,7 +49,22 @@ BoutReal limitFree(BoutReal fm, BoutReal fc) { } // namespace SheathBoundaryInsulating::SheathBoundaryInsulating(std::string name, Options& alloptions, - Solver*) { + Solver*) + : Component({ + readIfSet("species:{all_species}:charge"), + readIfSet("species:e:{e_whole_domain}"), + writeBoundaryFinal("species:e:{e_boundary}"), + readWrite("species:e:energy_source"), + writeBoundaryFinalIfSet("species:e:{e_optional}"), + writeBoundaryReadInteriorIfSet("species:e:pressure"), + readIfSet("species:{ions}:{ion_whole_domain}"), + readOnly("species:{ions}:AA"), + readWrite("species:{ions}:energy_source"), + writeBoundaryFinal("species:{ions}:{ion_boundary}"), + writeBoundaryFinalIfSet("species:{ions}:{ion_optional}"), + writeBoundaryReadInteriorIfSet("species:{ions}:pressure"), + writeBoundaryFinalIfSet("fields:phi") + }) { AUTO_TRACE(); Options& options = alloptions[name]; @@ -77,13 +92,20 @@ SheathBoundaryInsulating::SheathBoundaryInsulating(std::string name, Options& al gamma_e = options["gamma_e"] .doc("Electron sheath heat transmission coefficient") .withDefault(3.5); + + substitutePermissions("e_whole_domain", {"AA", "adiabatic"}); + substitutePermissions("e_boundary", {"density", "temperature"}); + substitutePermissions("e_optional", {"velocity", "momentum"}); + substitutePermissions("ion_whole_domain", {"charge", "adiabatic"}); + substitutePermissions("ion_boundary", {"density", "temperature"}); + substitutePermissions("ion_optional", {"velocity", "momentum"}); } -void SheathBoundaryInsulating::transform(Options& state) { +void SheathBoundaryInsulating::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; - Options& electrons = allspecies["e"]; + GuardedOptions allspecies = state["species"]; + GuardedOptions electrons = allspecies["e"]; // Need electron properties // Not const because boundary conditions will be set @@ -203,7 +225,7 @@ void SheathBoundaryInsulating::transform(Options& state) { continue; // Skip electrons } - Options& species = allspecies[kv.first]; // Note: Need non-const + GuardedOptions species = allspecies[kv.first]; // Note: Need non-const // Ion charge const BoutReal Zi = diff --git a/src/sheath_boundary_simple.cxx b/src/sheath_boundary_simple.cxx index 53da53230..d4057986d 100644 --- a/src/sheath_boundary_simple.cxx +++ b/src/sheath_boundary_simple.cxx @@ -57,8 +57,22 @@ BoutReal limitFree(BoutReal fm, BoutReal fc, BoutReal mode) { } // namespace -SheathBoundarySimple::SheathBoundarySimple(std::string name, Options& alloptions, - Solver*) { +SheathBoundarySimple::SheathBoundarySimple(std::string name, Options& alloptions, Solver*) + : Component({ + readIfSet("species:e:{e_whole_domain}"), + writeBoundaryFinal("species:e:{e_boundary}"), + readWrite("species:e:energy_source"), + readWrite("species:e:energy_flow_ylow"), + writeBoundaryFinalIfSet("species:e:{e_optional}"), + writeBoundaryReadInteriorIfSet("species:e:pressure"), + readIfSet("species:{ions}:{ion_whole_domain}"), + readOnly("species:{ions}:AA"), + readWrite("species:{ions}:energy_source"), + readWrite("species:{ions}:energy_flow_ylow"), + writeBoundaryFinal("species:{ions}:{ion_boundary}"), + writeBoundaryReadInteriorIfSet("species:{ions}:pressure"), + writeBoundaryFinalIfSet("species:{ions}:{ion_optional}"), + }) { AUTO_TRACE(); Options& options = alloptions[name]; @@ -130,14 +144,22 @@ SheathBoundarySimple::SheathBoundarySimple(std::string name, Options& alloptions diagnose = options["diagnose"] .doc("Save additional output diagnostics") .withDefault(false); - + + substitutePermissions("e_whole_domain", {"AA", "adiabatic"}); + substitutePermissions("e_boundary", {"density", "temperature"}); + substitutePermissions("e_optional", {"velocity", "momentum"}); + substitutePermissions("ion_whole_domain", {"charge", "adiabatic"}); + substitutePermissions("ion_boundary", {"density", "temperature"}); + substitutePermissions("ion_optional", {"velocity", "momentum"}); + setPermissions(always_set_phi ? writeBoundaryReadInteriorIfSet("fields:phi") + : writeBoundaryIfSet("fields:phi")); } -void SheathBoundarySimple::transform(Options& state) { +void SheathBoundarySimple::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; - Options& electrons = allspecies["e"]; + GuardedOptions allspecies = state["species"]; + GuardedOptions electrons = allspecies["e"]; // Need electron properties // Not const because boundary conditions will be set @@ -180,7 +202,7 @@ void SheathBoundarySimple::transform(Options& state) { // Iterate through charged ion species for (auto& kv : allspecies.getChildren()) { - const Options& species = kv.second; + const GuardedOptions species = kv.second; if ((kv.first == "e") or !species.isSet("charge") or (get(species["charge"]) == 0.0)) { @@ -492,7 +514,7 @@ void SheathBoundarySimple::transform(Options& state) { continue; // Skip electrons } - Options& species = allspecies[kv.first]; // Note: Need non-const + GuardedOptions species = allspecies[kv.first]; // Note: Need non-const // Ion charge const BoutReal Zi = species.isSet("charge") ? get(species["charge"]) : 0.0; @@ -690,8 +712,8 @@ void SheathBoundarySimple::transform(Options& state) { // Add the total sheath power flux to the tracker of y power flows add(species["energy_flow_ylow"], fromFieldAligned(ion_sheath_power_ylow)); - set(diagnostics[species.name()]["energy_source"], hflux_i); - set(diagnostics[species.name()]["particle_source"], particle_source); + set(diagnostics[kv.first]["energy_source"], hflux_i); + set(diagnostics[kv.first]["particle_source"], particle_source); } } diff --git a/src/sheath_closure.cxx b/src/sheath_closure.cxx index 585caec28..3484c6c33 100644 --- a/src/sheath_closure.cxx +++ b/src/sheath_closure.cxx @@ -1,7 +1,11 @@ #include "../include/sheath_closure.hxx" -SheathClosure::SheathClosure(std::string name, Options &alloptions, Solver *) { +SheathClosure::SheathClosure(std::string name, Options& alloptions, Solver*) + : Component({readOnly("fields:phi"), readOnly("species:e:density"), + readWrite("species:e:density_source"), + // FIXME: This is only written if temperature is set + readWrite("species:e:energy_source"), readWrite("fields:DivJextra")}) { Options& options = alloptions[name]; BoutReal Lnorm = alloptions["units"]["meters"]; // Length normalisation factor @@ -29,15 +33,25 @@ SheathClosure::SheathClosure(std::string name, Options &alloptions, Solver *) { .withDefault(false); output.write("\tL_par = {:e} (normalised)\n", L_par); + + if (sinks) { + setPermissions(readOnly("species:e:temperature")); + setPermissions(readOnly("species:{non_electrons}:{inputs}")); + setPermissions(readWrite("species:{non_electrons}:{outputs}")); + substitutePermissions("inputs", {"AA", "density", "temperature"}); + substitutePermissions("output", {"density_source", "energy_source"}); + } else { + setPermissions(readIfSet("species:e:temperature")); + } } -void SheathClosure::transform(Options &state) { +void SheathClosure::transform_impl(GuardedOptions& state) { AUTO_TRACE(); // Get electrostatic potential auto phi = get(state["fields"]["phi"]); - auto& electrons = state["species"]["e"]; + auto electrons = state["species"]["e"]; // Electron density auto n = get(electrons["density"]); @@ -70,10 +84,11 @@ void SheathClosure::transform(Options &state) { // standard Bohm boundary conditions for a pure, hydrogenic plasma.] Field3D P_total = 0.0; Field3D rho_total = 0.0; // mass density - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; + GuardedOptions species = allspecies[kv.first]; + // FIXME: This includes electrons in the calculation. Is that desired? const BoutReal A = get(species["AA"]); Field3D Ns = get(species["density"]); Field3D Ts = get(species["temperature"]); @@ -85,7 +100,7 @@ void SheathClosure::transform(Options &state) { Field3D c_s = sqrt(P_total / rho_total); for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; + GuardedOptions species = allspecies[kv.first]; Field3D Ns = get(species["density"]); Field3D sheath_flux = floor(Ns * c_s, 0.0); diff --git a/src/snb_conduction.cxx b/src/snb_conduction.cxx index 36521355f..f7c40a3b9 100644 --- a/src/snb_conduction.cxx +++ b/src/snb_conduction.cxx @@ -4,14 +4,9 @@ #include using bout::globals::mesh; -void SNBConduction::transform(Options& state) { - auto& units = state["units"]; - const auto rho_s0 = get(units["meters"]); - const auto Tnorm = get(units["eV"]); - const auto Nnorm = get(units["inv_meters_cubed"]); - const auto Omega_ci = 1. / get(units["seconds"]); +void SNBConduction::transform_impl(GuardedOptions& state) { - Options& electrons = state["species"]["e"]; + GuardedOptions electrons = state["species"]["e"]; // Note: Needs boundary conditions on temperature const Field3D Te = GET_VALUE(Field3D, electrons["temperature"]) * Tnorm; // eV const Field3D Ne = GET_VALUE(Field3D, electrons["density"]) * Nnorm; // In m^-3 diff --git a/src/solkit_hydrogen_charge_exchange.cxx b/src/solkit_hydrogen_charge_exchange.cxx index 6c16f269c..949d86bf2 100644 --- a/src/solkit_hydrogen_charge_exchange.cxx +++ b/src/solkit_hydrogen_charge_exchange.cxx @@ -2,7 +2,7 @@ #include "../include/integrate.hxx" // for cellAverage -void SOLKITHydrogenChargeExchange::calculate_rates(Options& atom, Options& ion) { +void SOLKITHydrogenChargeExchange::calculate_rates(GuardedOptions atom, GuardedOptions ion) { const auto AA = get(ion["AA"]); // Check that mass is consistent ASSERT1(get(atom["AA"]) == AA); diff --git a/src/solkit_neutral_parallel_diffusion.cxx b/src/solkit_neutral_parallel_diffusion.cxx index 0555d9a97..76676859a 100644 --- a/src/solkit_neutral_parallel_diffusion.cxx +++ b/src/solkit_neutral_parallel_diffusion.cxx @@ -6,12 +6,12 @@ using bout::globals::mesh; -void SOLKITNeutralParallelDiffusion::transform(Options& state) { +void SOLKITNeutralParallelDiffusion::transform_impl(GuardedOptions& state) { AUTO_TRACE(); - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (auto& kv : allspecies.getChildren()) { // Get non-const reference - auto& species = allspecies[kv.first]; + auto species = allspecies[kv.first]; if (species.isSet("charge") and (get(species["charge"]) != 0.0)) { // Skip charged species @@ -25,8 +25,8 @@ void SOLKITNeutralParallelDiffusion::transform(Options& state) { // Start with no collisions Field3D(0.0), [this](Field3D value, - const std::map::value_type &name_species) { - const Options &species = name_species.second; + const std::map::value_type &name_species) { + const GuardedOptions species = name_species.second; if (name_species.first == "e") { // Electrons const Field3D Ne = GET_VALUE(Field3D, species["density"]); diff --git a/src/sound_speed.cxx b/src/sound_speed.cxx index eaa7d9335..c4ce23fc9 100644 --- a/src/sound_speed.cxx +++ b/src/sound_speed.cxx @@ -3,13 +3,13 @@ #include "../include/hermes_utils.hxx" #include -void SoundSpeed::transform(Options &state) { +void SoundSpeed::transform_impl(GuardedOptions& state) { Field3D total_pressure = 0.0; Field3D total_density = 0.0; Field3D fastest_wave = 0.0; for (auto& kv : state["species"].getChildren()) { - const Options& species = kv.second; + const GuardedOptions species = kv.second; if (species.isSet("pressure")) { total_pressure += GET_NOBOUNDARY(Field3D, species["pressure"]); diff --git a/src/temperature_feedback.cxx b/src/temperature_feedback.cxx index bf226e37c..a868e231e 100644 --- a/src/temperature_feedback.cxx +++ b/src/temperature_feedback.cxx @@ -3,8 +3,8 @@ #include using bout::globals::mesh; -void TemperatureFeedback::transform(Options& state) { - Options& species = state["species"][name]; +void TemperatureFeedback::transform_impl(GuardedOptions& state) { + GuardedOptions species = state["species"][name]; // Doesn't need all boundaries to be set Field3D T = getNoBoundary(species["temperature"]); diff --git a/src/transform.cxx b/src/transform.cxx index 77842118a..7f439f93d 100644 --- a/src/transform.cxx +++ b/src/transform.cxx @@ -3,7 +3,8 @@ #include // for trim, strsplit -Transform::Transform(std::string name, Options& alloptions, Solver* UNUSED(solver)) { +Transform::Transform(std::string name, Options& alloptions, Solver* UNUSED(solver)) + : Component({readOnly("{inputs}"), writeFinal("{outputs}")}) { Options& options = alloptions[name]; @@ -12,6 +13,8 @@ Transform::Transform(std::string name, Options& alloptions, Solver* UNUSED(solve const auto str = trim( options["transforms"].doc("Comma-separated list e.g. a = b, c = d"), trim_chars); + std::vector inputs, outputs; + for (const auto& assign_str : strsplit(str, ',')) { auto assign_lr = strsplit(assign_str, '='); if (assign_lr.size() != 2) { @@ -22,11 +25,16 @@ Transform::Transform(std::string name, Options& alloptions, Solver* UNUSED(solve const auto right = trim(assign_lr.back(), trim_chars); transforms[left] = right; + inputs.push_back(left); + outputs.push_back(right); } + + substitutePermissions("inputs", inputs); + substitutePermissions("outputs", outputs); } -void Transform::transform(Options& state) { +void Transform::transform_impl(GuardedOptions& state) { for (const auto& lr : transforms) { - state[lr.first] = state[lr.second].copy(); + state[lr.first].getWritable() = state[lr.second].get().copy(); } } diff --git a/src/upstream_density_feedback.cxx b/src/upstream_density_feedback.cxx index f3acc0626..d5ff3c809 100644 --- a/src/upstream_density_feedback.cxx +++ b/src/upstream_density_feedback.cxx @@ -3,8 +3,8 @@ #include using bout::globals::mesh; -void UpstreamDensityFeedback::transform(Options& state) { - Options& species = state["species"][name]; +void UpstreamDensityFeedback::transform_impl(GuardedOptions& state) { + GuardedOptions species = state["species"][name]; // Doesn't need all boundaries to be set Field3D N = getNoBoundary(species["density"]); diff --git a/src/vorticity.cxx b/src/vorticity.cxx index e663d73e7..a9bcef324 100644 --- a/src/vorticity.cxx +++ b/src/vorticity.cxx @@ -40,7 +40,8 @@ BoutReal limitFree(BoutReal fm, BoutReal fc) { } } // namespace -Vorticity::Vorticity(std::string name, Options& alloptions, Solver* solver) { +Vorticity::Vorticity(std::string name, Options& alloptions, Solver* solver) + : Component({readWrite("fields:vorticity"), readWrite("fields:phi")}) { AUTO_TRACE(); solver->add(Vort, "Vort"); @@ -205,13 +206,42 @@ Vorticity::Vorticity(std::string name, Options& alloptions, Solver* solver) { diagnose = options["diagnose"] .doc("Output additional diagnostics?") .withDefault(false); + + if (diamagnetic or diamagnetic_polarisation) { + // FIXME: These will only be read if BOTH charge and pressure (and possibly AA) are + // set + setPermissions(readIfSet("species:{charged}:pressure", Regions::Interior)); + setPermissions(readIfSet("species:{all_species}:charge")); + } + if (diamagnetic) { + setPermissions(readWrite("species:{charged}:energy_source")); + setPermissions(readWrite("fields:DivJdia")); + } + if (diamagnetic_polarisation or collisional_friction) { + // FIXME: Only read if pressure also set + setPermissions(readIfSet("species:{charged}:AA")); + } + if (phi_boundary_relax) { + setPermissions(readOnly("time")); + } else { + if (sheath_boundary) { + setPermissions(readOnly("species:e:AA")); + } + setPermissions(readIfSet("species:e:temperature", Regions::Interior)); + } + if (collisional_friction) { + setPermissions(readIfSet("species:{all_species}:charge")); + setPermissions(readOnly("species:{positive_ions}:density")); + setPermissions(readIfSet("species:{positive_ions}:collision_frequency")); + setPermissions(readWrite("fields:DivJcol")); + } } -void Vorticity::transform(Options& state) { +void Vorticity::transform_impl(GuardedOptions& state) { AUTO_TRACE(); phi.name = "phi"; - auto& fields = state["fields"]; + auto fields = state["fields"]; // Set the boundary of phi. Both 2D and 3D fields are kept, though the 3D field // is constant in Z. This is for efficiency, to reduce the number of conversions. @@ -221,9 +251,9 @@ void Vorticity::transform(Options& state) { if (diamagnetic_polarisation) { // Diamagnetic term in vorticity. Note this is weighted by the mass // This includes all species, including electrons - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: need non-const + GuardedOptions species = allspecies[kv.first]; // Note: need non-const if (!(IS_SET_NOBOUNDARY(species["pressure"]) and species.isSet("charge") and species.isSet("AA"))) { @@ -482,10 +512,10 @@ void Vorticity::transform(Options& state) { Jdia.z = 0.0; Jdia.covariant = Curlb_B.covariant; - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (auto& kv : allspecies.getChildren()) { - Options& species = allspecies[kv.first]; // Note: need non-const + GuardedOptions species = allspecies[kv.first]; // Note: need non-const if (!(IS_SET_NOBOUNDARY(species["pressure"]) and IS_SET(species["charge"]))) { continue; // No pressure or charge -> no diamagnetic current @@ -587,9 +617,9 @@ void Vorticity::transform(Options& state) { zeroFrom(Vort); // Sum of atomic mass * collision frequency * density Field3D sum_A_n = zeroFrom(Vort); // Sum of atomic mass * density - const Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (const auto& kv : allspecies.getChildren()) { - const Options& species = kv.second; + const GuardedOptions species = kv.second; if (!(species.isSet("charge") and species.isSet("AA"))) { continue; // No charge or mass -> no current diff --git a/src/zero_current.cxx b/src/zero_current.cxx index 37757b116..4c20ba1ea 100644 --- a/src/zero_current.cxx +++ b/src/zero_current.cxx @@ -4,28 +4,33 @@ #include "../include/zero_current.hxx" ZeroCurrent::ZeroCurrent(std::string name, Options& alloptions, Solver*) - : name(name) { + : Component({readIfSet("species:{all_species}:charge"), + readIfSet("species:{all_species}:{inputs}", Regions::Interior), + readWrite(fmt::format("species:{}:velocity", name))}), + name(name) { AUTO_TRACE(); Options &options = alloptions[name]; charge = options["charge"].doc("Particle charge. electrons = -1"); ASSERT0(charge != 0.0); + + substitutePermissions("inputs", {"density", "velocity"}); } -void ZeroCurrent::transform(Options &state) { +void ZeroCurrent::transform_impl(GuardedOptions& state) { AUTO_TRACE(); // Current due to other species Field3D current; // Now calculate forces on other species - Options& allspecies = state["species"]; + GuardedOptions allspecies = state["species"]; for (auto& kv : allspecies.getChildren()) { if (kv.first == name) { continue; // Skip self } - Options& species = allspecies[kv.first]; // Note: Need non-const + GuardedOptions species = allspecies[kv.first]; // Note: Need non-const if (!(species.isSet("density") and species.isSet("charge"))) { continue; // Needs both density and charge to contribute @@ -55,8 +60,8 @@ void ZeroCurrent::transform(Options &state) { } // Get the species density - Options& species = state["species"][name]; - if (species["velocity"].isSet()) { + GuardedOptions species = state["species"][name]; + if (species.isSet("velocity")) { throw BoutException("Cannot use zero_current in species {} if velocity already set\n", name); } Field3D N = getNoBoundary(species["density"]); diff --git a/tests/integrated/component-order/data/BOUT.inp b/tests/integrated/component-order/data/BOUT.inp new file mode 100644 index 000000000..2614b19c8 --- /dev/null +++ b/tests/integrated/component-order/data/BOUT.inp @@ -0,0 +1,164 @@ +nout = 0 +timestep = 1 + + +[mesh] +# 1D simulation, use "y" as the dimension along the fieldline +nx = 10 +ny = 10 # Resolution along field-line +nz = 10 +ixseps1 = 0 +J = 1 + + +[hermes] +components = (d+, he+, he, d, e, + sheath_boundary_simple, braginskii_collisions, braginskii_friction, + braginskii_heat_exchange, reactions, electron_force_balance, + neutral_parallel_diffusion, braginskii_ion_viscosity, + braginskii_conduction, recycling) + + +[solver] +type = pvode + + +[sheath_boundary_simple] +lower_y = false +upper_y = true +diagnose = true + + +[neutral_parallel_diffusion] +dneut = 10 +diffusion_collisions_mode = multispecies + + +[braginskii_collisions] +electron_ion = true +electron_electron = true +electron_neutral = true +ion_ion = true +ion_neutral = true +neutral_neutral = true +diagnose = true + + +[braginskii_ion_viscosity] +viscosity_collisions_mode = multispecies + +[recycling] +species = d+, he+ + + +#################################### +[d+] +type = (evolve_density, evolve_pressure, evolve_momentum, + noflow_boundary) + +charge = 1 +AA = 2 +thermal_conduction = true +conduction_collisions_mode = multispecies +noflow_upper_y = false +recycle_as = d +target_recycle = true +diagnose = true + +[Nd+] +function = 1 + +[Pd+] +function = 1 + +[NVd+] +function = 1 + +#################################### +[he+] +type = (evolve_density, evolve_pressure, evolve_momentum, + noflow_boundary) + +charge = 2 +AA = 4 +thermal_conduction = true +conduction_collisions_mode = multispecies +noflow_upper_y = false +recycle_as = he +target_recycle = true +diagnose = true + +[Nhe+] +function = 0.01 + +[Phe+] +function = 0.001 + +[NVhe+] +function = 0.001 + +#################################### +[he] +type = (neutral_mixed, noflow_boundary) + +AA = 4 +diffusion_collisions_mode = multispecies +diagnose = true + +[Nhe] +function = 0.001 + +[Phe] +function = 0.0001 + +[NVhe] +function = 0.0001 + +#################################### +[d] +type = (neutral_mixed, noflow_boundary) + +AA = 2 +diffusion_collisions_mode = multispecies +diagnose = true + +[Nd] +function = 0.001 + +[Pd] +function = 0.0001 + +[NVd] +function = 0.0001 + +#################################### +[e] # Electrons +type = (quasineutral, evolve_pressure, zero_current, + noflow_boundary) + +noflow_upper_y = false + +charge = -1 +AA = 1/1836 +thermal_conduction = true # in evolve_pressure +conduction_collisions_mode = multispecies + +diagnose = true + +[Pe] + +function = `Pd+:function` + +#################################### + + +[reactions] +diagnose = true +type = ( + d + e -> d+ + 2e, # Deuterium ionisation + d+ + e -> d, # Deuterium recombination + d + d+ -> d+ + d, # Charge exchange + + he + e -> he+ + 2e, # Helium ionisation + he+ + e -> he, # Helium+ recombination + ) diff --git a/tests/integrated/component-order/runtest b/tests/integrated/component-order/runtest new file mode 100755 index 000000000..8b653027d --- /dev/null +++ b/tests/integrated/component-order/runtest @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +# Python script to run and analyse two test cases that are identical +# except the order in which the components are specified. + +from __future__ import division +from __future__ import print_function + +try: + from builtins import str +except: + pass + +from boututils.run_wrapper import shell, launch_safe, getmpirun +from boutdata.collect import collect +import numpy +import xhermes + +from numpy import sqrt, max, abs, mean, array, log, concatenate +from pathlib import Path + + +def check_value(name, val, target, **kws): + # override default tolerances to get desired behaviour + kws["atol"] = kws.pop("atol", 0.0) + kws["rtol"] = kws.pop("rtol", 0.0) + + success = numpy.isclose(val, target, **kws) + if not success: + # On failure, report the effective abs tolerance used by isclose + atol_eff = kws["atol"] + kws["rtol"] * abs(target) + print( + f"Expected\n {target-atol_eff} < {name} < {target+atol_eff}\nBut actual value was\n {val}" + ) + return success + + +cases = [ + "d+, he+, he, d, e, sheath_boundary_simple, braginskii_collisions, braginskii_friction, braginskii_heat_exchange, reactions, electron_force_balance, neutral_parallel_diffusion, braginskii_ion_viscosity, braginskii_conduction, recycling", + "recycling, d+, braginskii_friction, braginskii_heat_exchange, he, reactions, electron_force_balance, neutral_parallel_diffusion, d, braginskii_ion_viscosity, sheath_boundary_simple, he+, braginskii_conduction, braginskii_collisions, e" +] + +# Remove old test results and symlink executable + +if not Path("hermes-3").is_file(): + shell("ln -s ../../../hermes-3 hermes-3") + +results = [] + +# Run and exit if unsuccessful +for i, c in enumerate(cases): + for file in ["BOUT.dmp.0.nc", "BOUT.log.0", "BOUT.restart.0.nc", "BOUT.settings", ".BOUT.pid.0"]: + path = Path("data") / file + if path.is_file(): + print(f"Removing old file data/{file}") + path.unlink() + + s, out = launch_safe(f"./hermes-3 hermes:components='{c}'", nproc=1, pipe=True) + + if s != 0: + print(" => Test failed: ") + print(out) + exit(1) + + # Save output to log file + with open(f"run.{i}.log", "w") as f: + f.write(out) + + results.append(xhermes.open("data", unnormalise=False).isel(t=-1)) + +success = True + +rtol = 1e-10 +atol = 1e-10 +diagnostics = ["Nd+", "Pd+", "Vd+", "NVd+", "Nd", "Pd", "Vd", "NVd", "Nhe+", "Phe+", "Vhe+", "NVhe+", "Nhe", "Phe", "Vhe", "NVhe", "Ne", "Pe", "Ve"] +idx = (3, 5, 7) + +failing = [] + +# Check output is the same between the simulations +for d in diagnostics: + if not check_value(d, results[0][d].values[idx], results[1][d].values[idx], rtol=rtol, atol=atol): + success = False + failing.append(d) + + +# Final output +if success: + print(" => Test passed") + exit(0) +else: + print(f" => Test failed: \nDisagreement in diagnostic(s) {', '.join(failing)}") + exit(1) diff --git a/tests/unit/test_amjueldata.cxx b/tests/unit/test_amjueldata.cxx index b4d97ad6d..df2aa60d6 100644 --- a/tests/unit/test_amjueldata.cxx +++ b/tests/unit/test_amjueldata.cxx @@ -23,7 +23,7 @@ static Options valid_options{ /// @brief Test that setting an invalid json db dir throws. TEST(AmjuelDataTest, InvalidCustomDataDir) { Options options{{"json_database_dir", "/invalid/file/path"}}; - ASSERT_THROW(AmjuelReaction("dummy_name", "iz", "test", "dummy_from_species", + ASSERT_THROW(AmjuelReaction("dummy_name", "iz", "test", "dummy_from_species -> dummy_to_species", "dummy_from_species", "dummy_to_species", options), BoutException); } @@ -32,7 +32,7 @@ TEST(AmjuelDataTest, InvalidCustomDataDir) { TEST(AmjuelDataTest, InValidFilename) { std::string invalid_amjuel_lbl = "invalid_lbl"; if (std::filesystem::is_directory(test_json_db_path)) { - ASSERT_THROW(AmjuelReaction("test", "valid", invalid_amjuel_lbl, "dummy_from_species", + ASSERT_THROW(AmjuelReaction("test", "valid", invalid_amjuel_lbl, "dummy_from_species -> dummy_to_species", "dummy_from_species", "dummy_to_species", valid_options), BoutException); } else { @@ -45,7 +45,7 @@ TEST(AmjuelDataTest, InValidFilename) { /// @brief Test that trying to read invalid data throws. TEST(AmjuelDataTest, InValidData) { if (std::filesystem::is_directory(test_json_db_path)) { - ASSERT_THROW(AmjuelReaction("test", "invalid", "test", "dummy_from_species", + ASSERT_THROW(AmjuelReaction("test", "invalid", "test", "dummy_from_species -> dummy_to_species", "dummy_from_species", "dummy_to_species", valid_options), BoutException); } else { @@ -58,7 +58,7 @@ TEST(AmjuelDataTest, InValidData) { /// Test that json_database_dir can be overridden with a valid path TEST(AmjuelDataTest, ValidCustomDataDir) { if (std::filesystem::is_directory(test_json_db_path)) { - ASSERT_NO_THROW(AmjuelReaction("test", "valid", "test", "dummy_from_species", + ASSERT_NO_THROW(AmjuelReaction("test", "valid", "test", "dummy_from_species -> dummy_to_species", "dummy_from_species", "dummy_to_species", valid_options)); } else { // If tests are run on a filesystem where repo path isn't accessible, just skip @@ -70,7 +70,7 @@ TEST(AmjuelDataTest, ValidCustomDataDir) { /// Test that reading data without coefficients works TEST(AmjuelDataTest, ValidNoSigmavEData) { if (std::filesystem::is_directory(test_json_db_path)) { - ASSERT_NO_THROW(AmjuelReaction("test", "valid_no-sigma-v-E", "test", + ASSERT_NO_THROW(AmjuelReaction("test", "valid_no-sigma-v-E", "test", "dummy_from_species -> dummy_to_species", "dummy_from_species", "dummy_to_species", valid_options)); } else { @@ -78,4 +78,4 @@ TEST(AmjuelDataTest, ValidNoSigmavEData) { GTEST_SKIP() << "Couldn't access test json db dir at " << test_json_db_path << ", skipping!"; } -} \ No newline at end of file +} diff --git a/tests/unit/test_braginskii_closure.cxx b/tests/unit/test_braginskii_closure.cxx new file mode 100644 index 000000000..3f9da557f --- /dev/null +++ b/tests/unit/test_braginskii_closure.cxx @@ -0,0 +1,89 @@ + +#include "gtest/gtest.h" + +#include "fake_mesh_fixture.hxx" +#include "test_extras.hxx" // FakeMesh + +#include "../../include/braginskii_closure.hxx" + +/// Global mesh +namespace bout { +namespace globals { +extern Mesh* mesh; +} // namespace globals +} // namespace bout + +// The unit tests use the global mesh +using namespace bout::globals; + +// Reuse the "standard" fixture for FakeMesh +using BraginskiiClosureTest = FakeMeshFixture; + +std::set makeExpected(std::initializer_list names) { + std::set result; + for (const auto& name : names) { + result.emplace(name, name); + } + return result; +} + +std::set toSet(std::vector input) { + return std::set(input.begin(), input.end()); +} + +TEST_F(BraginskiiClosureTest, CreateDefault) { + Options options; + BraginskiiClosure component("test", options, nullptr); + EXPECT_EQ(toSet(component.additionalComponents()), + makeExpected({"braginskii_collisions", "braginskii_friction", + "braginskii_heat_exchange", "braginskii_conduction", + "braginskii_electron_viscosity", "braginskii_ion_viscosity", + "braginskii_thermal_force"})); +} + +TEST_F(BraginskiiClosureTest, CreateMinimal) { + Options options = {{"test", + {{"electron_viscosity", false}, + {"ion_viscosity", false}, + {"thermal_force", false}}}}; + BraginskiiClosure component("test", options, nullptr); + EXPECT_EQ(toSet(component.additionalComponents()), + makeExpected({"braginskii_collisions", "braginskii_friction", + "braginskii_heat_exchange", "braginskii_conduction"})); +} + +TEST_F(BraginskiiClosureTest, CreateThermalForce) { + Options options = {{"test", + {{"electron_viscosity", false}, + {"ion_viscosity", false}, + {"thermal_force", true}}}}; + BraginskiiClosure component("test", options, nullptr); + EXPECT_EQ(toSet(component.additionalComponents()), + makeExpected({"braginskii_collisions", "braginskii_friction", + "braginskii_heat_exchange", "braginskii_conduction", + "braginskii_thermal_force"})); +} + +TEST_F(BraginskiiClosureTest, CreateIonViscosity) { + Options options = {{"test", + {{"electron_viscosity", false}, + {"ion_viscosity", true}, + {"thermal_force", false}}}}; + BraginskiiClosure component("test", options, nullptr); + EXPECT_EQ(toSet(component.additionalComponents()), + makeExpected({"braginskii_collisions", "braginskii_friction", + "braginskii_heat_exchange", "braginskii_conduction", + "braginskii_ion_viscosity"})); +} + +TEST_F(BraginskiiClosureTest, CreateElectronViscosity) { + Options options = {{"test", + {{"electron_viscosity", true}, + {"ion_viscosity", false}, + {"thermal_force", false}}}}; + BraginskiiClosure component("test", options, nullptr); + EXPECT_EQ(toSet(component.additionalComponents()), + makeExpected({"braginskii_collisions", "braginskii_friction", + "braginskii_heat_exchange", "braginskii_conduction", + "braginskii_electron_viscosity"})); +} diff --git a/tests/unit/test_braginskii_collisions.cxx b/tests/unit/test_braginskii_collisions.cxx index 56462d4b7..1ad9b0a99 100644 --- a/tests/unit/test_braginskii_collisions.cxx +++ b/tests/unit/test_braginskii_collisions.cxx @@ -46,6 +46,7 @@ TEST_F(BraginskiiCollisionsTest, OnlyElectrons) { state["species"]["e"]["density"] = 1e19; state["species"]["e"]["temperature"] = 10.; + component.declareAllSpecies(SpeciesInformation({"e"}, {}, {}, {})); component.transform(state); ASSERT_TRUE(state["species"]["e"].isSet("collision_frequency")); @@ -67,7 +68,6 @@ TEST_F(BraginskiiCollisionsTest, OneOrTwoSpeciesCharged) { state1["species"]["s1"]["charge"] = 1; state1["species"]["s1"]["AA"] = 2; - // State with two species, both the same but half the density Options state2; state2["species"]["s1"]["density"] = 5e18; // Half density state2["species"]["s1"]["temperature"] = 10; @@ -77,6 +77,7 @@ TEST_F(BraginskiiCollisionsTest, OneOrTwoSpeciesCharged) { state2["species"]["s2"] = state2["species"]["s1"].copy(); // Run calculations + component.declareAllSpecies(SpeciesInformation({}, {}, {"s1", "s2"}, {})); component.transform(state1); component.transform(state2); @@ -115,6 +116,7 @@ TEST_F(BraginskiiCollisionsTest, TnormDependence) { {"d+", {{"density", 2e19}, {"temperature", 20}, {"charge", 1}, {"AA", 2}}}, {"d", {{"density", 1e18}, {"temperature", 3}, {"AA", 2}}}}}}; + component.declareAllSpecies(SpeciesInformation({"e"}, {"d"}, {"d+"}, {})); component.transform(state); ASSERT_TRUE(state["species"]["e"].isSet("collision_frequency")); @@ -154,6 +156,7 @@ TEST_F(BraginskiiCollisionsTest, TnormDependence) { {{"density", 2e19}, {"temperature", 20 / Tnorm}, {"charge", 1}, {"AA", 2}}}, {"d", {{"density", 1e18}, {"temperature", 3 / Tnorm}, {"AA", 2}}}}}}; + component2.declareAllSpecies(SpeciesInformation({"e"}, {"d"}, {"d+"}, {})); component2.transform(state2); // Normalised frequencies should be unchanged diff --git a/tests/unit/test_braginskii_conduction.cxx b/tests/unit/test_braginskii_conduction.cxx index 4caa87e1b..5337f679e 100644 --- a/tests/unit/test_braginskii_conduction.cxx +++ b/tests/unit/test_braginskii_conduction.cxx @@ -91,6 +91,7 @@ TEST_F(BraginskiiConductionTest, ConductionGradientScaling) { state1["species"]["test+"]["temperature"] = this->temp1; state2["species"]["test+"]["temperature"] = this->temp2; + component.declareAllSpecies({"test+"}); component.transform(state0); component.transform(state1); component.transform(state2); @@ -116,7 +117,9 @@ TEST_F(BraginskiiConductionTest, ConductionKappaScaling) { state1["species"]["test+"]["temperature"] = this->temp1; Options state2 = state1.copy(); + component1.declareAllSpecies({"test+"}); component1.transform(state1); + component2.declareAllSpecies({"test+"}); component2.transform(state2); Field3D conduction1 = this->getDeriv(state1); @@ -146,6 +149,7 @@ TEST_F(BraginskiiConductionTest, ConductionCollisionScaling) { state2["species"]["test+"]["collision_frequencies"]["test+_test+_coll"] = 2.; state2["species"]["test+"]["collision_frequency"] = 2.; + component.declareAllSpecies({"test+"}); component.transform(state0); component.transform(state1); component.transform(state2); @@ -172,7 +176,9 @@ TEST_F(BraginskiiConductionTest, ConductionCollisionsMode) { state_brag["species"]["test+"]["collision_frequency"] = 1.0; Options state_multi = state_brag.copy(); + component_braginskii.declareAllSpecies({"test+"}); component_braginskii.transform(state_brag); + component_multispecies.declareAllSpecies({"test+", "e"}); component_multispecies.transform(state_multi); Field3D conduction_brag = this->getDeriv(state_brag); diff --git a/tests/unit/test_braginskii_friction.cxx b/tests/unit/test_braginskii_friction.cxx index 6bdac0c0d..798ed460b 100644 --- a/tests/unit/test_braginskii_friction.cxx +++ b/tests/unit/test_braginskii_friction.cxx @@ -36,6 +36,7 @@ TEST_F(BraginskiiFrictionTest, OnlyElectrons) { state["species"]["e"]["AA"] = 1. / 1836; state["species"]["e"]["collision_frequencies"]["e_e_coll"] = 1.; + component.declareAllSpecies({"e"}); component.transform(state); // A species can't exert friction on itself, so momentum and energy transfer won't be @@ -73,6 +74,7 @@ TEST_F(BraginskiiFrictionTest, TwoComovingSpeciesCharged) { state["species"]["s2"]["collision_frequencies"]["s2_s1_coll"] = 0.25; // Run calculations + component.declareAllSpecies({"s1", "s2"}); component.transform(state); ASSERT_TRUE(state["species"]["s1"].isSet("momentum_source")); ASSERT_TRUE(state["species"]["s2"].isSet("momentum_source")); @@ -119,6 +121,7 @@ TEST_F(BraginskiiFrictionTest, TwoSpeciesCharged) { state["species"]["s2"]["collision_frequencies"]["s2_s1_coll"] = 0.25; // Run calculations + component.declareAllSpecies({"s1", "s2"}); component.transform(state); Field3D ms1 = get(state["species"]["s1"]["momentum_source"]); @@ -166,6 +169,7 @@ TEST_F(BraginskiiFrictionTest, DoubleRelativeVelocities) { state2["species"]["s2"]["velocity"] = 3; // Run calculations + component.declareAllSpecies({"s1", "s2"}); component.transform(state1); component.transform(state2); @@ -214,6 +218,7 @@ TEST_F(BraginskiiFrictionTest, TwoSpeciesNoHeating) { state["species"]["s2"]["velocity"] = 2; // Run calculations + component.declareAllSpecies({"s1", "s2"}); component.transform(state); ASSERT_TRUE(state["species"]["s1"]["momentum_source"].isSet()); @@ -251,6 +256,7 @@ TEST_F(BraginskiiFrictionTest, DoubleCollisionRate) { state2["species"]["s2"]["collision_frequencies"]["s2_s1_coll"] = 1.0; // Run calculations + component.declareAllSpecies({"s1", "s2"}); component.transform(state1); component.transform(state2); diff --git a/tests/unit/test_braginskii_heat_exchange.cxx b/tests/unit/test_braginskii_heat_exchange.cxx index e1b5ec388..a749bd987 100644 --- a/tests/unit/test_braginskii_heat_exchange.cxx +++ b/tests/unit/test_braginskii_heat_exchange.cxx @@ -36,6 +36,7 @@ TEST_F(BraginskiiHeatExchangeTest, OnlyElectrons) { state["species"]["e"]["AA"] = 1. / 1836; state["species"]["e"]["collision_frequencies"]["e_e_coll"] = 1.; + component.declareAllSpecies({"e"}); component.transform(state); // A species can't exchange heat with itself @@ -68,6 +69,7 @@ TEST_F(BraginskiiHeatExchangeTest, TwoEqualTempSpeciesCharged) { state["species"]["s2"]["collision_frequencies"]["s2_s1_coll"] = 0.25; // Run calculations + component.declareAllSpecies({"s1", "s2"}); component.transform(state); ASSERT_TRUE(state["species"]["s1"].isSet("energy_source")); ASSERT_TRUE(state["species"]["s2"].isSet("energy_source")); @@ -108,6 +110,7 @@ TEST_F(BraginskiiHeatExchangeTest, TwoSpeciesCharged) { state["species"]["s2"]["temperature"] = 20; // Run calculations + component.declareAllSpecies({"s1", "s2"}); component.transform(state); Field3D es1 = get(state["species"]["s1"]["energy_source"]); @@ -149,6 +152,7 @@ TEST_F(BraginskiiHeatExchangeTest, DoubleCollisionRates) { state2["species"]["s2"]["collision_frequencies"]["s2_s1_coll"] = 1.0; // Run calculations + component.declareAllSpecies({"s1", "s2"}); component.transform(state1); component.transform(state2); diff --git a/tests/unit/test_braginskii_ion_viscosity.cxx b/tests/unit/test_braginskii_ion_viscosity.cxx index c78b9e187..630712e1d 100644 --- a/tests/unit/test_braginskii_ion_viscosity.cxx +++ b/tests/unit/test_braginskii_ion_viscosity.cxx @@ -68,6 +68,7 @@ TEST_F(BraginskiiIonViscosityTest, ViscosityPressureScaling) { state2["species"]["d+"]["pressure"] = 2 * state1["species"]["d+"]["pressure"].as(); + component.declareAllSpecies({"d+", "c+"}); component.transform(state1); component.transform(state2); @@ -98,6 +99,7 @@ TEST_F(BraginskiiIonViscosityTest, ViscosityCollisionScaling) { state2["species"]["d+"]["collision_frequencies"]["d+_he+_coll"] = 2 * state1["species"]["d+"]["collision_frequencies"]["d+_he+_coll"].as(); + component.declareAllSpecies({"d+", "he+"}); component.transform(state1); component.transform(state2); @@ -129,6 +131,7 @@ TEST_F(BraginskiiIonViscosityTest, ViscosityVelocityScaling) { state2["species"]["d+"]["velocity"] = 2 * state1["species"]["d+"]["velocity"].as(); + component.declareAllSpecies({"d+", "c+"}); component.transform(state0); component.transform(state1); component.transform(state2); @@ -161,7 +164,9 @@ TEST_F(BraginskiiIonViscosityTest, ViscosityCollisionMode) { options2["test2"]["viscosity_collisions_mode"] = "braginskii"; BraginskiiIonViscosity component2("test2", options2, nullptr); + component.declareAllSpecies({"d+", "c+"}); component.transform(state1); + component2.declareAllSpecies({"d+", "c+"}); component2.transform(state2); Field3D visc1 = state1["species"]["d+"]["momentum_source"]; diff --git a/tests/unit/test_braginskii_thermal_force.cxx b/tests/unit/test_braginskii_thermal_force.cxx index b59aa48ff..25d3108fb 100644 --- a/tests/unit/test_braginskii_thermal_force.cxx +++ b/tests/unit/test_braginskii_thermal_force.cxx @@ -52,6 +52,7 @@ TEST_F(BraginskiiThermalForceTest, OnlyElectrons) { state["species"]["e"]["charge"] = -1; state["species"]["e"]["AA"] = 1. / 1836; + component.declareAllSpecies({"e"}); component.transform(state); EXPECT_FALSE(state["species"]["e"]["momentum_source"].isSet()); } @@ -63,6 +64,7 @@ TEST_F(BraginskiiThermalForceTest, OnlyOneIon) { state["species"]["d+"]["charge"] = 1; state["species"]["d+"]["AA"] = 2.; + component.declareAllSpecies({"d+"}); component.transform(state); EXPECT_FALSE(state["species"]["d+"]["momentum_source"].isSet()); } @@ -78,6 +80,7 @@ TEST_F(BraginskiiThermalForceTest, ElectronIonBalance) { state["species"]["d+"]["charge"] = 1; state["species"]["d+"]["AA"] = 2.; + component.declareAllSpecies({"e", "d+"}); component.transform(state); Field3D mom_e = state["species"]["e"]["momentum_source"]; @@ -90,18 +93,19 @@ TEST_F(BraginskiiThermalForceTest, ElectronIonBalance) { TEST_F(BraginskiiThermalForceTest, IonIonBalance) { Options state; - state["species"]["c"]["density"] = 0.01; - state["species"]["c"]["temperature"] = grad1; - state["species"]["c"]["charge"] = 1; - state["species"]["c"]["AA"] = 12.; + state["species"]["c+"]["density"] = 0.01; + state["species"]["c+"]["temperature"] = grad1; + state["species"]["c+"]["charge"] = 1; + state["species"]["c+"]["AA"] = 12.; state["species"]["d+"]["density"] = 1; state["species"]["d+"]["temperature"] = grad1; state["species"]["d+"]["charge"] = 1; state["species"]["d+"]["AA"] = 2.; + component.declareAllSpecies({"c+", "d+"}); component.transform(state); - Field3D mom_c = state["species"]["c"]["momentum_source"]; + Field3D mom_c = state["species"]["c+"]["momentum_source"]; Field3D mom_d = state["species"]["d+"]["momentum_source"]; BOUT_FOR_SERIAL(i, mom_c.getRegion("RGN_NOBNDRY")) { // Forces on the species should be equal and opposite @@ -111,14 +115,14 @@ TEST_F(BraginskiiThermalForceTest, IonIonBalance) { TEST_F(BraginskiiThermalForceTest, NoNetForce) { Options state; - state["species"]["c"]["density"] = 0.01; - state["species"]["c"]["temperature"] = grad1; - state["species"]["c"]["charge"] = 1; - state["species"]["c"]["AA"] = 12.; - state["species"]["ar"]["density"] = 0.01; - state["species"]["ar"]["temperature"] = grad2; - state["species"]["ar"]["charge"] = 1; - state["species"]["ar"]["AA"] = 40.; + state["species"]["c+"]["density"] = 0.01; + state["species"]["c+"]["temperature"] = grad1; + state["species"]["c+"]["charge"] = 1; + state["species"]["c+"]["AA"] = 12.; + state["species"]["ar+"]["density"] = 0.01; + state["species"]["ar+"]["temperature"] = grad2; + state["species"]["ar+"]["charge"] = 1; + state["species"]["ar+"]["AA"] = 40.; state["species"]["d"]["density"] = 1; state["species"]["d"]["temperature"] = grad1; state["species"]["d"]["charge"] = 0; @@ -132,6 +136,7 @@ TEST_F(BraginskiiThermalForceTest, NoNetForce) { state["species"]["e"]["charge"] = -1; state["species"]["e"]["AA"] = 1. / 1836; + component.declareAllSpecies({"c+", "ar+", "d", "d+", "e"}); component.transform(state); Field3D force(0.); for (const auto& [name, species] : state["species"].subsections()) { @@ -159,6 +164,7 @@ TEST_F(BraginskiiThermalForceTest, ElectronForceDensityScaling) { state["species"]["e"]["charge"] = -1; state["species"]["e"]["AA"] = 1. / 1836; + component.declareAllSpecies({"d1+", "d2+", "e"}); component.transform(state); Field3D mom1 = state["species"]["d1+"]["momentum_source"]; Field3D mom2 = state["species"]["d2+"]["momentum_source"]; @@ -184,6 +190,7 @@ TEST_F(BraginskiiThermalForceTest, ElectronForceChargeScaling) { state["species"]["e"]["charge"] = -1; state["species"]["e"]["AA"] = 1. / 1836; + component.declareAllSpecies({"d1+", "d2+", "e"}); component.transform(state); Field3D mom1 = state["species"]["d1+"]["momentum_source"]; Field3D mom2 = state["species"]["d2+"]["momentum_source"]; @@ -218,6 +225,7 @@ TEST_F(BraginskiiThermalForceTest, ElectronForceTemperatureGradScaling) { state1["species"]["d2+"]["charge"] = 1; state1["species"]["d2+"]["AA"] = 2.; + component.declareAllSpecies({"d+", "d2+", "e"}); component.transform(state0); component.transform(state1); component.transform(state2); @@ -239,14 +247,14 @@ TEST_F(BraginskiiThermalForceTest, ElectronForceTemperatureGradScaling) { TEST_F(BraginskiiThermalForceTest, IonIonForceTemperatureGradScaling) { Options state1; Options state2; - state1["species"]["c1"]["density"] = 0.01; - state1["species"]["c1"]["temperature"] = grad1; - state1["species"]["c1"]["charge"] = 1; - state1["species"]["c1"]["AA"] = 12.; - state1["species"]["c2"]["density"] = 0.01; - state1["species"]["c2"]["temperature"] = grad2; - state1["species"]["c2"]["charge"] = 1; - state1["species"]["c2"]["AA"] = 12.; + state1["species"]["c1+"]["density"] = 0.01; + state1["species"]["c1+"]["temperature"] = grad1; + state1["species"]["c1+"]["charge"] = 1; + state1["species"]["c1+"]["AA"] = 12.; + state1["species"]["c2+"]["density"] = 0.01; + state1["species"]["c2+"]["temperature"] = grad2; + state1["species"]["c2+"]["charge"] = 1; + state1["species"]["c2+"]["AA"] = 12.; state1["species"]["d+"]["density"] = 1; state1["species"]["d+"]["charge"] = 1; state1["species"]["d+"]["AA"] = 2.; @@ -255,13 +263,14 @@ TEST_F(BraginskiiThermalForceTest, IonIonForceTemperatureGradScaling) { state1["species"]["d+"]["temperature"] = grad1; state2["species"]["d+"]["temperature"] = grad2; + component.declareAllSpecies({"d+", "c1+", "c2+"}); component.transform(state1); component.transform(state2); - Field3D mom1_1 = state1["species"]["c1"]["momentum_source"]; - Field3D mom1_2 = state1["species"]["c2"]["momentum_source"]; - Field3D mom2_1 = state2["species"]["c1"]["momentum_source"]; - Field3D mom2_2 = state2["species"]["c2"]["momentum_source"]; + Field3D mom1_1 = state1["species"]["c1+"]["momentum_source"]; + Field3D mom1_2 = state1["species"]["c2+"]["momentum_source"]; + Field3D mom2_1 = state2["species"]["c1+"]["momentum_source"]; + Field3D mom2_2 = state2["species"]["c2+"]["momentum_source"]; BOUT_FOR_SERIAL(i, mom1_1.getRegion("RGN_NOBNDRY")) { // Changing temperature gradient of the heavy ion should not change the force EXPECT_DOUBLE_EQ(mom1_1[i], mom1_2[i]); @@ -279,12 +288,13 @@ TEST_P(BraginskiiThermalForceTest_MassRatio, CheckForIonMasses) { auto [aa1, aa2, thermal_force_present] = GetParam(); Options state{ {"species", - {{"M", {{"density", 1}, {"temperature", grad1}, {"charge", 1}, {"AA", aa1}}}, - {"N", {{"density", 1}, {"temperature", grad2}, {"charge", 2}, {"AA", aa2}}}}}}; + {{"M+", {{"density", 1}, {"temperature", grad1}, {"charge", 1}, {"AA", aa1}}}, + {"N+", {{"density", 1}, {"temperature", grad2}, {"charge", 2}, {"AA", aa2}}}}}}; + component.declareAllSpecies({"M+", "N+"}); component.transform(state); if (thermal_force_present) { - Field3D momentum_source1 = state["species"]["M"]["momentum_source"]; - Field3D momentum_source2 = state["species"]["N"]["momentum_source"]; + Field3D momentum_source1 = state["species"]["M+"]["momentum_source"]; + Field3D momentum_source2 = state["species"]["N+"]["momentum_source"]; BOUT_FOR_SERIAL(i, momentum_source1.getRegion("RGN_NOBNDRY")) { // The masses of the ions are different enough for thermal force to be present. @@ -292,8 +302,8 @@ TEST_P(BraginskiiThermalForceTest_MassRatio, CheckForIonMasses) { EXPECT_NE(momentum_source2[i], 0.); } } else { - EXPECT_FALSE(state["species"]["M"]["momentum_source"].isSet()); - EXPECT_FALSE(state["species"]["N"]["momentum_source"].isSet()); + EXPECT_FALSE(state["species"]["M+"]["momentum_source"].isSet()); + EXPECT_FALSE(state["species"]["N+"]["momentum_source"].isSet()); } } diff --git a/tests/unit/test_component.cxx b/tests/unit/test_component.cxx index 0a62fa64c..f8ec11144 100644 --- a/tests/unit/test_component.cxx +++ b/tests/unit/test_component.cxx @@ -7,8 +7,13 @@ namespace { struct TestComponent : public Component { - TestComponent(const std::string&, Options&, Solver *) {} - void transform(Options &state) override { state["answer"] = 42; } + TestComponent(const std::string&, Options&, Solver*) + : Component({readWrite("answer")}) {} + +private: + void transform_impl(GuardedOptions& state) override { + state["answer"].getWritable() = 42; + } }; RegisterComponent registertestcomponent("testcomponent"); diff --git a/tests/unit/test_component_scheduler.cxx b/tests/unit/test_component_scheduler.cxx index f3f05bd3d..5cda9961b 100644 --- a/tests/unit/test_component_scheduler.cxx +++ b/tests/unit/test_component_scheduler.cxx @@ -4,15 +4,22 @@ namespace { struct TestComponent : public Component { - TestComponent(const std::string&, Options&, Solver *) {} - void transform(Options &state) override { state["answer"] = 42; } + TestComponent(const std::string&, Options&, Solver*) + : Component({readWrite("answer")}) {} + +private: + void transform_impl(GuardedOptions& state) override { + state["answer"].getWritable() = 42; + } }; struct TestMultiply : public Component { - TestMultiply(const std::string&, Options&, Solver *) {} - - void transform(Options &state) override { + TestMultiply(const std::string&, Options&, Solver*) + : Component({writeFinal("answer")}) {} + +private: + void transform_impl(GuardedOptions& state) override { // Note: Using set<>() and get<>() for quicker access, avoiding printing // getNonFinal needs to be used because we set the value afterwards set(state["answer"], @@ -20,8 +27,39 @@ struct TestMultiply : public Component { } }; +struct TestAdditionalComponent : public Component { + TestAdditionalComponent(const std::string&, Options&, Solver*) + : Component({}) {} + + void transform_impl(GuardedOptions&) override { + } + + std::vector additionalComponents() override { + return {{"TestComponent", "testcomponent"}, {"component2", "multiply"}}; + } +}; + +struct OrderChecker : public Component { + OrderChecker(const std::string& name, Options& alloptions, Solver*) + : Component(getPermissions(name, alloptions)), name(name) {} + static Permissions getPermissions(const std::string& name, Options& alloptions) { + return alloptions[name]["permissions"].as(); + } + static void resetOrderInfo() { execution_order.clear(); } + + std::string name; + static std::vector execution_order; + +private: + void transform_impl(GuardedOptions&) override { execution_order.push_back(name); } +}; + +std::vector OrderChecker::execution_order; + RegisterComponent registertestcomponent("testcomponent"); RegisterComponent registertestcomponent2("multiply"); +RegisterComponent registertestcomponent3("additionalcomponent"); +RegisterComponent registercomponentorderchecker("orderchecker"); } // namespace TEST(SchedulerTest, OneComponent) { @@ -59,3 +97,275 @@ TEST(SchedulerTest, SubComponents) { ASSERT_TRUE(options["answer"] == 42 * 2); } +TEST(SchedulerTest, AdditionalComponents) { + Options options; + options["components"] = "additionalcomponent"; + auto scheduler = ComponentScheduler::create(options, options, nullptr); + + EXPECT_FALSE(options.isSet("answer")); + scheduler->transform(options); + ASSERT_TRUE(options.isSet("answer")); + ASSERT_TRUE(options["answer"] == 42 * 2); +} + +TEST(SchedulerTest, AdditionalComponentsPredeclared) { + Options options; + options["components"] = "testcomponent, additionalcomponent"; + auto scheduler = ComponentScheduler::create(options, options, nullptr); + + EXPECT_FALSE(options.isSet("answer")); + scheduler->transform(options); + ASSERT_TRUE(options.isSet("answer")); + ASSERT_TRUE(options["answer"] == 42 * 2); +} + +TEST(SchedulerTest, AdditionalComponentsPredeclared2) { + Options options; + options["components"] = "additionalcomponent, testcomponent"; + auto scheduler = ComponentScheduler::create(options, options, nullptr); + + EXPECT_FALSE(options.isSet("answer")); + scheduler->transform(options); + ASSERT_TRUE(options.isSet("answer")); + ASSERT_TRUE(options["answer"] == 42 * 2); +} + +using Parameter = std::pair>; + +class ComponentOrderTest : public testing::TestWithParam { + void SetUp() override { OrderChecker::resetOrderInfo(); } +}; + +TEST_P(ComponentOrderTest, Sorted) { + Options options = GetParam().first.copy(); + auto scheduler = ComponentScheduler::create(options, options, nullptr); + scheduler->transform(options); + EXPECT_EQ(OrderChecker::execution_order, GetParam().second); +} + +INSTANTIATE_TEST_SUITE_P( + TopologicalSort, ComponentOrderTest, + testing::Values( + Parameter({{"components", ""}}, {}), + Parameter( + {{"components", "a"}, + {"a", {{"type", "orderchecker"}, {"permissions", toString(Permissions())}}}}, + {"a"}), + Parameter({{"components", "a"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("1"), readWrite("2")}))}}}}, + {"a"}), + Parameter( + {{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("1"), readWrite("2")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readOnly("1"), readOnly("2")}))}}}}, + {"a", "b"}), + Parameter({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("1"), readWrite("2"), + readOnly("time")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readOnly("1"), readOnly("2"), + readIfSet("linear"), + readOnly("units:eV")}))}}}}, + {"a", "b"}), + Parameter({{"components", "b,a"}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readOnly("1"), readOnly("2")}))}}}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("1"), readWrite("2")}))}}}}, + {"a", "b"}), + Parameter( + {{"components", "b,a,c"}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readOnly("1"), readOnly("2")}))}}}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("1"), readWrite("2")}))}}}, + {"c", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("2"), readOnly("1")}))}}}}, + {"a", "c", "b"}), + Parameter({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readIfSet("1")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("1")}))}}}}, + {"b", "a"}), + Parameter({{"components", "a,b,c"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({writeFinal("1"), readIfSet("3")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("1"), readOnly("2")}))}}}, + {"c", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("1"), readWrite("2"), + readIfSet("3")}))}}}}, + {"c", "b", "a"}), + Parameter({{"components", "a,b,c"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({writeFinal("1:1_1"), readWrite("1:1_2")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("1")}))}}}, + {"c", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readOnly("1:1_1")}))}}}}, + {"b", "a", "c"}), + Parameter( + {{"components", "a,b,c"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({writeFinal("1"), readOnly("2")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("2:2_1"), writeFinal("2:2_2")}))}}}, + {"c", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("1"), readOnly("2:2_1"), + readIfSet("3")}))}}}}, + {"b", "c", "a"}), + Parameter({{"components", "a,b,c"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readOnly("1"), readWrite("1:1_1")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("1:1_1"), readWrite("1:1_2")}))}}}, + {"c", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readOnly("1:1_2")}))}}}}, + {"b", "a", "c"}), + Parameter({{"components", "a,b,c"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({writeBoundaryFinal("1")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readOnly("1")}))}}}, + {"c", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("1", Regions::Interior)}))}}}}, + {"c", "a", "b"}), + Parameter({{"components", "b,a,c"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readIfSet("1", Regions::Interior), + readWrite("2", Regions::All)}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({writeFinal("1", Regions::Boundaries), + readIfSet("2", Regions::Interior)}))}}}, + {"c", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readOnly("1", Regions::Boundaries), + readOnly("2", Regions::Boundaries)}))}}}}, + {"a", "b", "c"}), + Parameter({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({ + readOnly("1"), + }))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({writeFinal("1:1_1"), + readIfSet("1:1_2")}))}}}}, + {"b", "a"}))); + +class InvalidComponentOrderTest : public testing::TestWithParam { + void SetUp() override { OrderChecker::resetOrderInfo(); } +}; + +TEST_P(InvalidComponentOrderTest, BadDAG) { + Options options = GetParam().copy(); + EXPECT_THROW(ComponentScheduler::create(options, options, nullptr), BoutException); +} + +INSTANTIATE_TEST_SUITE_P( + InvalidTopologicalSort, InvalidComponentOrderTest, + testing::Values( + // Unsatisfiable dependency + Options({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readOnly("1"), readWrite("2")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readWrite("3")}))}}}}), + // Multiple final writes + Options({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({writeFinal("1")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({writeFinal("1")}))}}}}), + // Circular dependency + Options({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readOnly("1"), readWrite("2")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("2"), readOnly("1")}))}}}}), + // Circular dependency from readIfSet + Options({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readIfSet("1"), readWrite("2")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readOnly("2"), readWrite("1")}))}}}}), + // Unsatisfiable dependency due to only setting one region + Options({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readOnly("1")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("1", Regions::Interior)}))}}}}), + // Circular dependency on only one region + Options({{"components", "a,b"}, + {"a", + {{"type", "orderchecker"}, + {"permissions", toString(Permissions({readOnly("1", Regions::Interior), + readWrite("2")}))}}}, + {"b", + {{"type", "orderchecker"}, + {"permissions", + toString(Permissions({readWrite("1"), readOnly("2")}))}}}}))); diff --git a/tests/unit/test_electron_force_balance.cxx b/tests/unit/test_electron_force_balance.cxx index e194944cf..d2a4b7b62 100644 --- a/tests/unit/test_electron_force_balance.cxx +++ b/tests/unit/test_electron_force_balance.cxx @@ -47,6 +47,7 @@ TEST_F(ElectronForceBalanceTest, ZeroPressureGradient) { options["species"]["h+"]["density"] = 1.0; options["species"]["h+"]["charge"] = 1.0; + component.declareAllSpecies({"e", "h+"}); component.transform(options); // Should have a momentum source, but zero because no pressure gradient @@ -69,6 +70,7 @@ TEST_F(ElectronForceBalanceTest, WithPressureGradient) { options["species"]["h+"]["density"] = 1.0; options["species"]["h+"]["charge"] = 1.0; + component.declareAllSpecies({"e", "h+"}); component.transform(options); // Should have a momentum source @@ -98,6 +100,7 @@ TEST_F(ElectronForceBalanceTest, ForceBalance) { options["species"]["ion"]["density"] = 1.0; options["species"]["ion"]["charge"] = 3.0; + component.declareAllSpecies({"e", "ion"}); component.transform(options); // Should give ion momentum source charge * E = 3 * 0.5 / 2.0 diff --git a/tests/unit/test_evolve_pressure.cxx b/tests/unit/test_evolve_pressure.cxx index e55eecc9a..c41778682 100644 --- a/tests/unit/test_evolve_pressure.cxx +++ b/tests/unit/test_evolve_pressure.cxx @@ -63,8 +63,10 @@ TEST_F(EvolvePressureTest, Finally) { {"pressure", 1.0}, {"temperature", 1.0}, {"pressure_source", 0.5}}}}}}; +#if CHECKLEVEL >= 1 // Throws exception due to pressure_source EXPECT_THROW(component.finally(state), BoutException); +#endif const Options state2 = {{"species", {{"i", diff --git a/tests/unit/test_guarded_options.cxx b/tests/unit/test_guarded_options.cxx new file mode 100644 index 000000000..c4399adc2 --- /dev/null +++ b/tests/unit/test_guarded_options.cxx @@ -0,0 +1,269 @@ +#include +#include + +#include +#include +#include "gtest/gtest.h" + +#include "../include/guarded_options.hxx" +#include "../include/permissions.hxx" + +class GuardedOptionsTests : public testing::Test { +protected: + GuardedOptionsTests() + : permissions( + {readIfSet("species:he:AA"), + readIfSet("species:he:charge"), + readOnly("species:he:density"), + {"species:he:pressure", + {Regions::Nowhere, Regions::Nowhere, Regions::Interior, Regions::Nowhere}}, + writeFinal("species:he:collision_frequency"), + {"species:he:velocity", + {Regions::Nowhere, Regions::Boundaries, Regions::Nowhere, Regions::Nowhere}}, + readOnly("species:d"), + {"species:d:pressure", + {Regions::Nowhere, Regions::Nowhere, Regions::Interior, Regions::Nowhere}}, + {"species:d:collision_frequencies", + {Regions::Nowhere, Regions::Nowhere, Regions::Boundaries, Regions::Nowhere}}, + readIfSet("fields:phi"), + readOnly("unused:option")}), + opts({{"species", + {{"he", + {{"charge", 0}, + {"temperature", 0}, + {"density", 1}, + {"pressure", 2}, + {"velocity", 4}}}, + {"d", + {{"pressure", 5}, + {"velocity", 6}, + {"collision_frequencies", {{"d_d_coll", 7}, {"d_he_coll", 8}}}}}}}}), + guarded_opts(&opts, &permissions){}; + + Permissions permissions; + Options opts; + GuardedOptions guarded_opts; +}; + +TEST_F(GuardedOptionsTests, TestGet) { + EXPECT_EQ(guarded_opts["species:he:charge"].get(), 0); + EXPECT_EQ(guarded_opts["species:he:density"].get(), 1); + EXPECT_EQ(guarded_opts["species:he:density"].get(Regions::Boundaries), 1); + EXPECT_EQ(guarded_opts["species:he:pressure"].get(Regions::Interior), 2); + EXPECT_FALSE(guarded_opts["species:he:collision_frequency"].get().isSet()); + EXPECT_EQ(guarded_opts["species"]["he"]["velocity"].get(Regions::Boundaries), 4); + EXPECT_EQ(guarded_opts["species"]["d"]["pressure"].get(Regions::Interior), 5); + EXPECT_EQ(guarded_opts["species"]["d"]["velocity"].get(Regions::All), 6); + EXPECT_EQ(guarded_opts["species:d:collision_frequencies"]["d_d_coll"].get( + Regions::Boundaries), + 7); + EXPECT_EQ(guarded_opts["species:d:collision_frequencies"].get( + Regions::Boundaries)["d_he_coll"], + 8); + EXPECT_FALSE(guarded_opts["species:d:collision_frequencies:d_t+_cx"] + .get(Regions::Boundaries) + .isSet()); +} + +#if CHECKLEVEL >= 1 +TEST_F(GuardedOptionsTests, TestGetException) { + EXPECT_THROW(guarded_opts["species:he:AA"].get(), BoutException); + EXPECT_THROW(guarded_opts["species"]["he"]["temperature"].get(), BoutException); + EXPECT_THROW(guarded_opts["species:he:pressure"].get(), BoutException); + EXPECT_THROW(guarded_opts["species"]["he"]["velocity"].get(Regions::Interior), + BoutException); + EXPECT_THROW(guarded_opts["species:d:collision_frequencies"].get(Regions::Interior), + BoutException); + EXPECT_THROW(guarded_opts["species:d:pressure"].get(Regions::Boundaries), + BoutException); + EXPECT_THROW(guarded_opts["no_permission"].get(), BoutException); + EXPECT_THROW(guarded_opts["species:d+:velocity"].get(), BoutException); +} +#endif + +TEST_F(GuardedOptionsTests, TestGetWritable) { + auto& he_pressure = guarded_opts["species:he:pressure"].getWritable(Regions::Interior); + EXPECT_EQ(he_pressure, 2); + EXPECT_EQ(opts["species"]["he"]["pressure"], 2); + he_pressure.force(10); + EXPECT_EQ(he_pressure, 10); + EXPECT_EQ(opts["species"]["he"]["pressure"], 10); + + auto& he_freq = guarded_opts["species"]["he"]["collision_frequency"].getWritable(); + EXPECT_FALSE(he_freq.isSet()); + he_freq = 11; + EXPECT_EQ(opts["species"]["he"]["collision_frequency"], 11); + + auto& d_pressure = + guarded_opts["species"]["d"]["pressure"].getWritable(Regions::Interior); + EXPECT_EQ(opts["species"]["d"]["pressure"], 5); + d_pressure.force(12); + EXPECT_EQ(opts["species"]["d"]["pressure"], 12); + + auto& d_d_coll = guarded_opts["species:d:collision_frequencies:d_d_coll"].getWritable( + Regions::Boundaries); + EXPECT_EQ(d_d_coll, 7); + d_d_coll.force(13); + EXPECT_EQ(opts["species:d:collision_frequencies:d_d_coll"], 13); + + auto& d_tp_coll = guarded_opts["species:d:collision_frequencies:d_t+_coll"].getWritable( + Regions::Boundaries); + EXPECT_FALSE(d_tp_coll.isSet()); + d_tp_coll = 14; + EXPECT_EQ(opts["species:d:collision_frequencies:d_t+_coll"], 14); +} + +#if CHECKLEVEL >= 1 +TEST_F(GuardedOptionsTests, TestGetWritableException) { + EXPECT_THROW(guarded_opts["species"]["he"]["temperature"].getWritable(), BoutException); + EXPECT_THROW(guarded_opts["unset"].getWritable(), BoutException); + EXPECT_THROW(guarded_opts["species:he:density"].getWritable(), BoutException); + EXPECT_THROW(guarded_opts["species:he:density"].getWritable(Regions::Interior), + BoutException); + EXPECT_THROW(guarded_opts["species:he:density"].getWritable(Regions::Boundaries), + BoutException); + EXPECT_THROW(guarded_opts["species:he:pressure"].getWritable(), BoutException); + EXPECT_THROW(guarded_opts["species:he:pressure"].getWritable(Regions::Boundaries), + BoutException); + EXPECT_THROW(guarded_opts["species"]["d"]["velocity"].getWritable(), BoutException); + EXPECT_THROW(guarded_opts["species"]["d"]["pressure"].getWritable(Regions::Boundaries), + BoutException); + EXPECT_THROW(guarded_opts["species:d:collision_frequencies:unset"].getWritable(), + BoutException); + EXPECT_THROW(guarded_opts["species:d:collision_frequencies:unset"].getWritable( + Regions::Interior), + BoutException); + EXPECT_THROW( + guarded_opts["species"]["d"]["pressure_suffix"].getWritable(Regions::Interior), + BoutException); +} + +TEST_F(GuardedOptionsTests, TestUnreadItems) { + const std::map expected1 = { + {"species:he:charge", Regions::All}, + {"species:he:density", Regions::All}, + {"species:he:velocity", Regions::Boundaries}, + {"species:d", Regions::All}, + {"unused:option", Regions::All}}; + const std::map expected2 = { + {"species:he:charge", Regions::All}, + {"species:he:density", Regions::Boundaries}, + {"species:he:velocity", Regions::Boundaries}, + {"species:d", Regions::All}, + {"unused:option", Regions::All}}; + const std::map expected3 = {{"species:d", Regions::All}}; + const std::map expected4; + + EXPECT_EQ(guarded_opts.unreadItems(), expected1); + + guarded_opts["species:he:density"].get(Regions::Interior); + guarded_opts["species:d:pressure"].get(Regions::Interior); + guarded_opts["species:d:collision_frequencies:d_d_coll"].getWritable( + Regions::Boundaries); + EXPECT_EQ(guarded_opts.unreadItems(), expected2); + + guarded_opts["species"]["he"]["charge"].get(); + guarded_opts["species"]["he"]["density"].get(); + guarded_opts["species:he:velocity"].get(Regions::Boundaries); + EXPECT_FALSE(guarded_opts["unused"]["option"].get().isSet()); + EXPECT_EQ(guarded_opts.unreadItems(), expected3); + + guarded_opts["species:d:velocity"].get(); + EXPECT_EQ(guarded_opts.unreadItems(), expected4); +} + +TEST_F(GuardedOptionsTests, TestUnwrittenItems) { + const std::map expected1 = { + {"species:he:pressure", Regions::Interior}, + {"species:he:collision_frequency", Regions::All}, + {"species:d:pressure", Regions::Interior}, + {"species:d:collision_frequencies", Regions::Boundaries}}; + const std::map expected2 = { + {"species:he:collision_frequency", Regions::All}, + {"species:d:pressure", Regions::Interior}}; + const std::map expected3; + + EXPECT_EQ(guarded_opts.unwrittenItems(), expected1); + + guarded_opts["species:he:pressure"].get(Regions::Interior); + guarded_opts["species:he:collision_frequency"].get(); + EXPECT_EQ(guarded_opts.unwrittenItems(), expected1); + + guarded_opts["species"]["he"]["pressure"].getWritable(Regions::Interior); + guarded_opts["species:d:collision_frequencies:d_d_coll"].getWritable( + Regions::Boundaries); + EXPECT_EQ(guarded_opts.unwrittenItems(), expected2); + + guarded_opts["species:he:collision_frequency"].getWritable(); + guarded_opts["species:d:pressure"].getWritable(Regions::Interior); + EXPECT_EQ(guarded_opts.unwrittenItems(), expected3); +} +#endif + +TEST_F(GuardedOptionsTests, TestNullOptions) { + EXPECT_THROW(GuardedOptions(nullptr, &permissions), BoutException); +} + +TEST_F(GuardedOptionsTests, TestNullPermissions) { + EXPECT_THROW(GuardedOptions(&opts, nullptr), BoutException); +} + +TEST_F(GuardedOptionsTests, TestGetChildren) { + std::map guarded_children = guarded_opts["species"].getChildren(); + EXPECT_EQ(guarded_children.size(), 2); + EXPECT_EQ(guarded_children.count("he"), 1); + EXPECT_EQ(guarded_children.count("d"), 1); + EXPECT_EQ(&(guarded_children.at("d").get()), &(opts["species"]["d"])); +#if CHECKLEVEL >= 1 + // We do not have access to the whole "he" section + EXPECT_THROW(guarded_children.at("he").get(), BoutException); +#endif +} + +TEST_F(GuardedOptionsTests, TestIsThisSection) { + EXPECT_TRUE(guarded_opts.isSection()); + EXPECT_TRUE(guarded_opts["species:d:collision_frequencies"].isSection()); + EXPECT_FALSE(guarded_opts["species:he:temperature"].isSection()); + EXPECT_TRUE(guarded_opts["species"]["he"]["collision_frequency"].isSection()); +} + +TEST_F(GuardedOptionsTests, TestIsChildSection) { + EXPECT_TRUE(guarded_opts.isSection("")); + // Unforunately Operator::isSection does not support full paths + EXPECT_FALSE(guarded_opts.isSection("species:he")); + EXPECT_FALSE(guarded_opts["species"]["he"].isSection("temperature")); + EXPECT_FALSE(guarded_opts["species:he"].isSection("pressure")); + EXPECT_TRUE(guarded_opts["species:d"].isSection("collision_frequencies")); +} + +TEST_F(GuardedOptionsTests, TestIsThisSet) { + EXPECT_TRUE(guarded_opts["species"]["he"]["temperature"].isSet()); + EXPECT_TRUE(guarded_opts["species:he:pressure"].isSet()); + EXPECT_TRUE(guarded_opts["species"]["he"]["velocity"].isSet()); + EXPECT_FALSE(guarded_opts["species"]["he"]["collision_frequency"].isSet()); + EXPECT_FALSE(guarded_opts["unset"].isSet()); +} + +TEST_F(GuardedOptionsTests, TestIsChildSet) { + EXPECT_FALSE(guarded_opts["species:he:pressure"].isSet("test")); + EXPECT_FALSE(guarded_opts["species:he"].isSet("collision_frequency")); + EXPECT_TRUE(guarded_opts["species"]["he"].isSet("temperature")); + // Unforunately Operator::isSet does not support full paths + EXPECT_FALSE(guarded_opts.isSet("species:he:temperature")); + EXPECT_TRUE(guarded_opts["species"]["d"]["collision_frequencies"].isSet("d_d_coll")); +} + +TEST_F(GuardedOptionsTests, TestUnsetNotInChildren) { + // This is a test for a bug that was found in the initial implementation + auto children = guarded_opts.getChildren(); + EXPECT_EQ(children.count("fields"), 0); + EXPECT_EQ(children.count("unused"), 0); +} + +TEST_F(GuardedOptionsTests, TestEqual) { + EXPECT_EQ(guarded_opts, guarded_opts); + EXPECT_EQ(guarded_opts["species:he:pressure"], + guarded_opts["species"]["he"]["pressure"]); + EXPECT_NE(guarded_opts["species:he:pressure"], guarded_opts["species:he:velocity"]); + EXPECT_NE(guarded_opts, GuardedOptions(&opts, &permissions)); +} diff --git a/tests/unit/test_permissions.cxx b/tests/unit/test_permissions.cxx new file mode 100644 index 000000000..4988fe4ff --- /dev/null +++ b/tests/unit/test_permissions.cxx @@ -0,0 +1,419 @@ +#include +#include +#include +#include + +#include +#include "gtest/gtest.h" + +#include "../include/permissions.hxx" + +auto make_access = std::make_pair; +auto make_permission = std::make_pair; + +TEST(PermissionsTests, TestCanAccess) { + const Permissions example({ + readIfSet("species:he:charge"), + readOnly("species:he:density"), + // Read and write permissions for pressure in the interior region + {"species:he:pressure", + {Regions::Nowhere, Regions::Nowhere, Regions::Interior, Regions::Nowhere}}, + // Set the final value for collision frequency + writeFinal("species:he:collision_frequency"), + // Only allow reading of boundary velocity + {"species:he:velocity", + {Regions::Nowhere, Regions::Boundaries, Regions::Nowhere, Regions::Nowhere}}, + readOnly("species:d"), + {"species:d:pressure", + {Regions::Nowhere, Regions::Nowhere, Regions::Interior, Regions::Nowhere}}, + {"species:d:collision_frequencies", + {Regions::Nowhere, Regions::Nowhere, Regions::Boundaries, Regions::Nowhere}}, + }); + + auto no_access = make_access(false, ""); + + // Check whether we have read permission for the variables across the entire domain + EXPECT_EQ(example.canAccess("species:he:charge"), no_access); + EXPECT_EQ(example.canAccess("species:he:density"), + make_access(true, "species:he:density")); + EXPECT_EQ(example.canAccess("species:he:pressure"), no_access); + EXPECT_EQ(example.canAccess("species:he:collision_frequency"), + make_access(true, "species:he:collision_frequency")); + EXPECT_EQ(example.canAccess("species:he:velocity"), no_access); + EXPECT_EQ(example.canAccess("unset"), no_access); + + // Check whether we have ReadIfSet permissions + EXPECT_EQ(example.canAccess("species:he:charge", PermissionTypes::ReadIfSet), + make_access(true, "species:he:charge")); + EXPECT_EQ(example.canAccess("species:he:density", PermissionTypes::ReadIfSet), + make_access(true, "species:he:density")); + EXPECT_EQ(example.canAccess("unset", PermissionTypes::ReadIfSet), no_access); + + // Check whether we have write permission for the variables across the entire domain + EXPECT_EQ(example.canAccess("species:he:charge", PermissionTypes::Write), no_access); + EXPECT_EQ(example.canAccess("species:he:density", PermissionTypes::Write), no_access); + EXPECT_EQ(example.canAccess("species:he:pressure", PermissionTypes::Write), no_access); + EXPECT_EQ(example.canAccess("species:he:collision_frequency", PermissionTypes::Write), + make_access(true, "species:he:collision_frequency")); + EXPECT_EQ(example.canAccess("species:he:velocity", PermissionTypes::Write), no_access); + EXPECT_EQ(example.canAccess("unset", PermissionTypes::Write), no_access); + + // Check whether we have read permission at the boundaries + EXPECT_EQ( + example.canAccess("species:he:charge", PermissionTypes::Read, Regions::Boundaries), + no_access); + EXPECT_EQ( + example.canAccess("species:he:density", PermissionTypes::Read, Regions::Boundaries), + make_access(true, "species:he:density")); + EXPECT_EQ(example.canAccess("species:he:pressure", PermissionTypes::Read, + Regions::Boundaries), + no_access); + EXPECT_EQ(example.canAccess("species:he:collision_frequency", PermissionTypes::Read, + Regions::Boundaries), + make_access(true, "species:he:collision_frequency")); + EXPECT_EQ(example.canAccess("species:he:velocity", PermissionTypes::Read, + Regions::Boundaries), + make_access(true, "species:he:velocity")); + EXPECT_EQ(example.canAccess("unset", PermissionTypes::Read, Regions::Boundaries), + no_access); + + // Check permissions set for whole sections + EXPECT_EQ(example.canAccess("species:d:pressure"), no_access); + EXPECT_EQ(example.canAccess("species:d:pressure", PermissionTypes::Write), no_access); + EXPECT_EQ( + example.canAccess("species:d:pressure", PermissionTypes::Write, Regions::Interior), + make_access(true, "species:d:pressure")); + EXPECT_EQ(example.canAccess("species:d:velocity"), make_access(true, "species:d")); + EXPECT_EQ(example.canAccess("species:d:velocity", PermissionTypes::Write), no_access); + EXPECT_EQ( + example.canAccess("species:d:velocity", PermissionTypes::Read, Regions::Boundaries), + make_access(true, "species:d")); + EXPECT_EQ(example.canAccess("species:d:collision_frequencies:d_d_coll"), no_access); + EXPECT_EQ(example.canAccess("species:d:collision_frequencies:d_d_coll", + PermissionTypes::Write), + no_access); + EXPECT_EQ(example.canAccess("species:d:collision_frequencies:d_d_coll", + PermissionTypes::Write, Regions::Boundaries), + make_access(true, "species:d:collision_frequencies")); + + // Check permissions for a species that might be mistaken for one of + // the sections we've given permissions for + EXPECT_EQ(example.canAccess("species:d+"), no_access); + EXPECT_EQ(example.canAccess("species:d+", PermissionTypes::Write), no_access); + EXPECT_EQ(example.canAccess("species:d+", PermissionTypes::Read, Regions::Interior), + no_access); + EXPECT_EQ(example.canAccess("species:d+", PermissionTypes::Read, Regions::Boundaries), + no_access); +} + +TEST(PermissionsTests, TestGetHighestPermission) { + const Permissions example({ + {"species:he:charge", + {Regions::All, Regions::Nowhere, Regions::Nowhere, Regions::Nowhere}}, + {"species:he:density", + {Regions::Nowhere, Regions::All, Regions::Nowhere, Regions::Boundaries}}, + // Read and write permissions for pressure in the interior region + {"species:he:pressure", + {Regions::Nowhere, Regions::Boundaries, Regions::Interior, Regions::Nowhere}}, + // Set the final value for collision frequency + {"species:he:collision_frequency", + {Regions::Nowhere, Regions::Interior, Regions::Nowhere, Regions::All}}, + // Only allow reading of boundary velocity + {"species:he:velocity", + {Regions::Nowhere, Regions::Boundaries, Regions::Nowhere, Regions::Nowhere}}, + {"species:d", {Regions::Nowhere, Regions::All, Regions::Nowhere, Regions::Nowhere}}, + {"species:d:pressure", + {Regions::Nowhere, Regions::Nowhere, Regions::Interior, Regions::Nowhere}}, + {"species:d:collision_frequencies", + {Regions::Nowhere, Regions::Nowhere, Regions::Boundaries, Regions::Nowhere}}, + writeBoundary("fields:phi"), + writeBoundaryIfSet("species:he+:temperature"), + }); + + auto no_permission = make_permission(PermissionTypes::None, ""); + + // Get the highest permission that covers the entire domain + EXPECT_EQ(example.getHighestPermission("species:he:charge"), + make_permission(PermissionTypes::ReadIfSet, "species:he:charge")); + EXPECT_EQ(example.getHighestPermission("species:he:density"), + make_permission(PermissionTypes::Read, "species:he:density")); + EXPECT_EQ(example.getHighestPermission("species:he:pressure"), + make_permission(PermissionTypes::Read, "species:he:pressure")); + EXPECT_EQ(example.getHighestPermission("species:he:collision_frequency"), + make_permission(PermissionTypes::Final, "species:he:collision_frequency")); + EXPECT_EQ(example.getHighestPermission("species:he:velocity"), + make_permission(PermissionTypes::None, "species:he:velocity")); + EXPECT_EQ(example.getHighestPermission("species:d:pressure"), + make_permission(PermissionTypes::None, "species:d:pressure")); + EXPECT_EQ(example.getHighestPermission("species:d:velocity"), + make_permission(PermissionTypes::Read, "species:d")); + EXPECT_EQ(example.getHighestPermission("species:d:collision_frequencies:d_d_coll"), + make_permission(PermissionTypes::None, "species:d:collision_frequencies")); + EXPECT_EQ(example.getHighestPermission("unset"), no_permission); + EXPECT_EQ(example.getHighestPermission("fields:phi"), + make_permission(PermissionTypes::Read, "fields:phi")); + EXPECT_EQ(example.getHighestPermission("species:he+:temperature"), + make_permission(PermissionTypes::ReadIfSet, "species:he+:temperature")); + + // Get the highest permission on the boundaries + EXPECT_EQ(example.getHighestPermission("species:he:charge", Regions::Boundaries), + make_permission(PermissionTypes::ReadIfSet, "species:he:charge")); + EXPECT_EQ(example.getHighestPermission("species:he:density", Regions::Boundaries), + make_permission(PermissionTypes::Final, "species:he:density")); + EXPECT_EQ(example.getHighestPermission("species:he:pressure", Regions::Boundaries), + make_permission(PermissionTypes::Read, "species:he:pressure")); + EXPECT_EQ( + example.getHighestPermission("species:he:collision_frequency", Regions::Boundaries), + make_permission(PermissionTypes::Final, "species:he:collision_frequency")); + EXPECT_EQ(example.getHighestPermission("species:he:velocity", Regions::Boundaries), + make_permission(PermissionTypes::Read, "species:he:velocity")); + EXPECT_EQ(example.getHighestPermission("species:d:pressure", Regions::Boundaries), + make_permission(PermissionTypes::None, "species:d:pressure")); + EXPECT_EQ(example.getHighestPermission("species:d:velocity", Regions::Boundaries), + make_permission(PermissionTypes::Read, "species:d")); + EXPECT_EQ(example.getHighestPermission("species:d:collision_frequencies:d_d_coll", + Regions::Boundaries), + make_permission(PermissionTypes::Write, "species:d:collision_frequencies")); + EXPECT_EQ(example.getHighestPermission("unset", Regions::Boundaries), no_permission); + EXPECT_EQ(example.getHighestPermission("fields:phi", Regions::Boundaries), + make_permission(PermissionTypes::Write, "fields:phi")); + EXPECT_EQ(example.getHighestPermission("species:he+:temperature", Regions::Boundaries), + make_permission(PermissionTypes::Write, "species:he+:temperature")); + + // Get the highest permission on the interior + EXPECT_EQ(example.getHighestPermission("species:he:charge", Regions::Interior), + make_permission(PermissionTypes::ReadIfSet, "species:he:charge")); + EXPECT_EQ(example.getHighestPermission("species:he:density", Regions::Interior), + make_permission(PermissionTypes::Read, "species:he:density")); + EXPECT_EQ(example.getHighestPermission("species:he:pressure", Regions::Interior), + make_permission(PermissionTypes::Write, "species:he:pressure")); + EXPECT_EQ( + example.getHighestPermission("species:he:collision_frequency", Regions::Interior), + make_permission(PermissionTypes::Final, "species:he:collision_frequency")); + EXPECT_EQ(example.getHighestPermission("species:he:velocity", Regions::Interior), + make_permission(PermissionTypes::None, "species:he:velocity")); + EXPECT_EQ(example.getHighestPermission("species:d:pressure", Regions::Interior), + make_permission(PermissionTypes::Write, "species:d:pressure")); + EXPECT_EQ(example.getHighestPermission("species:d:velocity", Regions::Interior), + make_permission(PermissionTypes::Read, "species:d")); + EXPECT_EQ(example.getHighestPermission("species:d:collision_frequencies:d_d_coll", + Regions::Interior), + make_permission(PermissionTypes::None, "species:d:collision_frequencies")); + EXPECT_EQ(example.getHighestPermission("unset", Regions::Interior), no_permission); + EXPECT_EQ(example.getHighestPermission("fields:phi", Regions::Interior), + make_permission(PermissionTypes::Read, "fields:phi")); + EXPECT_EQ(example.getHighestPermission("species:he+:temperature", Regions::Interior), + make_permission(PermissionTypes::ReadIfSet, "species:he+:temperature")); + + // Check the permission for the "Nowhere" region is always "None" + EXPECT_EQ(example.getHighestPermission("species:he:charge", Regions::Nowhere), + no_permission); + EXPECT_EQ(example.getHighestPermission("species:he:density", Regions::Nowhere), + no_permission); + EXPECT_EQ(example.getHighestPermission("species:he:pressure", Regions::Nowhere), + no_permission); + EXPECT_EQ( + example.getHighestPermission("species:he:collision_frequency", Regions::Nowhere), + no_permission); + EXPECT_EQ(example.getHighestPermission("species:he:velocity", Regions::Nowhere), + no_permission); + EXPECT_EQ(example.getHighestPermission("species:d:pressure", Regions::Nowhere), + no_permission); + EXPECT_EQ(example.getHighestPermission("species:d:velocity", Regions::Nowhere), + no_permission); + EXPECT_EQ(example.getHighestPermission("species:d:collision_frequencies:d_d_coll", + Regions::Nowhere), + no_permission); + EXPECT_EQ(example.getHighestPermission("unset", Regions::Nowhere), no_permission); + EXPECT_EQ(example.getHighestPermission("fields:phi", Regions::Nowhere), no_permission); + EXPECT_EQ(example.getHighestPermission("species:he+:temperature", Regions::Nowhere), + no_permission); + + // Check permissions for a species that might be mistaken for one of + // the sections we've given permissions for + EXPECT_EQ(example.getHighestPermission("species:d+"), no_permission); + EXPECT_EQ(example.getHighestPermission("species:d+", Regions::Interior), no_permission); + EXPECT_EQ(example.getHighestPermission("species:d+", Regions::Boundaries), + no_permission); +} + +TEST(PermissionsTests, TestSetAccess) { + Permissions example({ + {"species:he:density", + {Regions::Nowhere, Regions::All, Regions::Nowhere, Regions::Nowhere}}, + // Read and write permissions for pressure in the interior region + {"species:he:pressure", + {Regions::Nowhere, Regions::Nowhere, Regions::Interior, Regions::Nowhere}}, + }); + + EXPECT_EQ(example.getHighestPermission("species:he:density"), + make_permission(PermissionTypes::Read, "species:he:density")); + example.setAccess("species:he:density", {Regions::Nowhere, Regions::Nowhere, + Regions::Boundaries, Regions::Nowhere}); + EXPECT_EQ(example.getHighestPermission("species:he:density"), + make_permission(PermissionTypes::None, "species:he:density")); + EXPECT_EQ(example.getHighestPermission("species:he:density", Regions::Boundaries), + make_permission(PermissionTypes::Write, "species:he:density")); + + EXPECT_EQ(example.getHighestPermission("species:he:pressure", Regions::Interior), + make_permission(PermissionTypes::Write, "species:he:pressure")); + EXPECT_EQ(example.getHighestPermission("species:he:pressure", Regions::All), + make_permission(PermissionTypes::None, "species:he:pressure")); + example.setAccess("species:he:pressure", + {Regions::Nowhere, Regions::All, Regions::Nowhere, Regions::Nowhere}); + EXPECT_EQ(example.getHighestPermission("species:he:pressure", Regions::Interior), + make_permission(PermissionTypes::Read, "species:he:pressure")); + EXPECT_EQ(example.getHighestPermission("species:he:pressure"), + make_permission(PermissionTypes::Read, "species:he:pressure")); + + EXPECT_EQ(example.getHighestPermission("unset", Regions::Interior), + make_permission(PermissionTypes::None, "")); + EXPECT_EQ(example.getHighestPermission("unset", Regions::Boundaries), + make_permission(PermissionTypes::None, "")); + example.setAccess("unset", {Regions::Nowhere, Regions::Interior, Regions::Nowhere, + Regions::Boundaries}); + EXPECT_EQ(example.getHighestPermission("unset", Regions::All), + make_permission(PermissionTypes::Read, "unset")); + EXPECT_EQ(example.getHighestPermission("unset", Regions::Boundaries), + make_permission(PermissionTypes::Final, "unset")); +} + +TEST(PermissionsTests, TestGetVariablesWithPermissions) { + const Permissions example( + {{"species:he:density", + {Regions::Nowhere, Regions::All, Regions::Nowhere, Regions::Boundaries}}, + // Read and write permissions for pressure in the interior region + {"species:he:pressure", + {Regions::Nowhere, Regions::Boundaries, Regions::Interior, Regions::Nowhere}}, + // Set the final value for collision frequency + {"species:he:collision_frequency", + {Regions::Nowhere, Regions::Interior, Regions::Nowhere, Regions::All}}, + // Only allow reading of boundary velocity + {"species:he:velocity", + {Regions::Nowhere, Regions::Boundaries, Regions::Nowhere, Regions::Nowhere}}}); + + auto read_only = example.getVariablesWithPermission(PermissionTypes::Read); + EXPECT_EQ(read_only.size(), 3); + EXPECT_EQ(read_only["species:he:density"], Regions::Interior); + EXPECT_EQ(read_only["species:he:pressure"], Regions::Boundaries); + EXPECT_EQ(read_only["species:he:velocity"], Regions::Boundaries); + + auto readable = example.getVariablesWithPermission(PermissionTypes::Read, false); + EXPECT_EQ(readable.size(), 4); + EXPECT_EQ(readable["species:he:density"], Regions::All); + EXPECT_EQ(readable["species:he:pressure"], Regions::All); + EXPECT_EQ(readable["species:he:collision_frequency"], Regions::All); + EXPECT_EQ(readable["species:he:velocity"], Regions::Boundaries); + + auto write_nonfinal = example.getVariablesWithPermission(PermissionTypes::Write, true); + EXPECT_EQ(write_nonfinal.size(), 1); + EXPECT_EQ(write_nonfinal["species:he:pressure"], Regions::Interior); + + auto writable = example.getVariablesWithPermission(PermissionTypes::Write, false); + EXPECT_EQ(writable.size(), 3); + EXPECT_EQ(writable["species:he:density"], Regions::Boundaries); + EXPECT_EQ(writable["species:he:pressure"], Regions::Interior); + EXPECT_EQ(writable["species:he:collision_frequency"], Regions::All); + + auto final_write = example.getVariablesWithPermission(PermissionTypes::Final); + EXPECT_EQ(final_write.size(), 2); + EXPECT_EQ(final_write["species:he:density"], Regions::Boundaries); + EXPECT_EQ(final_write["species:he:collision_frequency"], Regions::All); + + EXPECT_THROW(example.getVariablesWithPermission(PermissionTypes::None, true), + BoutException); + EXPECT_THROW(example.getVariablesWithPermission(PermissionTypes::None, false), + BoutException); +} + +TEST(PermissionsTests, TestSubstitute) { + Permissions example( + {{"species:{s1}:collision_frequencies:{s1}_{s2}_coll", + {Regions::Nowhere, Regions::All, Regions::Nowhere, Regions::Nowhere}}, + readIfSet("d")}); + + example.setAccess( + "{var}", {Regions::Nowhere, Regions::Nowhere, Regions::Interior, Regions::Nowhere}); + + example.substitute("s1", {"e", "d+"}); + example.substitute("s2", {"e", "d+"}); + + auto readable = example.getVariablesWithPermission(PermissionTypes::Read, false); + EXPECT_EQ(readable.size(), 5); + EXPECT_EQ(readable["{var}"], Regions::Interior); + EXPECT_EQ(readable["species:e:collision_frequencies:e_e_coll"], Regions::All); + EXPECT_EQ(readable["species:e:collision_frequencies:e_d+_coll"], Regions::All); + EXPECT_EQ(readable["species:d+:collision_frequencies:d+_e_coll"], Regions::All); + EXPECT_EQ(readable["species:d+:collision_frequencies:d+_d+_coll"], Regions::All); + + example.substitute("var", {"a", "b", "c", "d"}); + + auto writable = example.getVariablesWithPermission(PermissionTypes::Write); + EXPECT_EQ(writable.size(), 3); + EXPECT_EQ(writable["a"], Regions::Interior); + EXPECT_EQ(writable["b"], Regions::Interior); + EXPECT_EQ(writable["c"], Regions::Interior); + + EXPECT_EQ(example.getHighestPermission("d"), + make_permission(PermissionTypes::ReadIfSet, "d")); +} + +TEST(PermissionsTests, TestRemainingSubstitutions) { + const Permissions p1 = {readOnly("species:h+:density"), readWrite("fields:phi")}; + const Permissions p2 = {readOnly("species:{all_species}:density"), + writeFinal("fields:phi")}; + const Permissions p3 = {readOnly("species:{all_species}:{inputs}"), + readIfSet("species:{ions}:{maybe_inputs}")}; + const Permissions p4 = {}; + p1.checkNoRemainingSubstitutions(); + EXPECT_THROW(p2.checkNoRemainingSubstitutions(), BoutException); + EXPECT_THROW(p3.checkNoRemainingSubstitutions(), BoutException); + p4.checkNoRemainingSubstitutions(); +} + +TEST(PermissionsTests, TestIO) { + const Permissions empty({}); + const Permissions single({readOnly("test")}); + const Permissions multiple( + {readIfSet("a", Regions::Interior), writeBoundaryFinal("b"), readWrite("c:d")}); + Permissions new_perm; + + std::stringstream ss1; + std::stringstream ss2; + std::stringstream ss3; + + ss1 << empty; + ss1 >> new_perm; + EXPECT_EQ(new_perm.getVariablesWithPermission(PermissionTypes::ReadIfSet, false).size(), + 0); + + ss2 << single; + ss2 >> new_perm; + EXPECT_EQ(new_perm.getVariablesWithPermission(PermissionTypes::ReadIfSet, false).size(), + 1); + std::map read_only = + new_perm.getVariablesWithPermission(PermissionTypes::Read); + EXPECT_EQ(read_only.size(), 1); + EXPECT_EQ(read_only["test"], Regions::All); + + ss3 << multiple; + ss3 >> new_perm; + EXPECT_EQ(new_perm.getVariablesWithPermission(PermissionTypes::ReadIfSet, false).size(), + 3); + std::map read_if_set = + new_perm.getVariablesWithPermission(PermissionTypes::ReadIfSet); + EXPECT_EQ(read_if_set.size(), 1); + EXPECT_EQ(read_if_set["a"], Regions::Interior); + std::map read = + new_perm.getVariablesWithPermission(PermissionTypes::Read); + EXPECT_EQ(read.size(), 1); + EXPECT_EQ(read["b"], Regions::Interior); + std::map read_write = + new_perm.getVariablesWithPermission(PermissionTypes::Write); + EXPECT_EQ(read_write.size(), 1); + EXPECT_EQ(read_write["c:d"], Regions::All); + std::map write_final = + new_perm.getVariablesWithPermission(PermissionTypes::Final); + EXPECT_EQ(write_final.size(), 1); + EXPECT_EQ(write_final["b"], Regions::Boundaries); +} diff --git a/tests/unit/test_sheath_boundary.cxx b/tests/unit/test_sheath_boundary.cxx index b6100466d..86ec45560 100644 --- a/tests/unit/test_sheath_boundary.cxx +++ b/tests/unit/test_sheath_boundary.cxx @@ -40,19 +40,19 @@ TEST_F(SheathBoundaryTest, DontSetPotential) { BoutReal Ti = 3.0; BoutReal Zi = 1.1; BoutReal si = 0.5; - - Options state {{"species", - {// Electrons - {"e", {{"density", N}, - {"temperature", Te}, - {"velocity", 0.0}}}, - // Ion species - {"h", {{"density", si*N}, - {"temperature", Ti}, - {"AA", 1.0}, - {"charge", Zi}, - {"velocity", 0.0}}}}}}; + Options state{{"species", + {// Electrons + {"e", {{"density", N}, {"temperature", Te}, {"velocity", 0.0}}}, + // Ion species + {"h+", + {{"density", si * N}, + {"temperature", Ti}, + {"AA", 1.0}, + {"charge", Zi}, + {"velocity", 0.0}}}}}}; + + component.declareAllSpecies({"e", "h+"}); component.transform(state); // Should have calculated, but not set potential @@ -75,13 +75,14 @@ TEST_F(SheathBoundaryTest, CalculatePotential) { {// Electrons {"e", {{"density", N}, {"temperature", Te}, {"velocity", 0.0}}}, // Ion species - {"h", + {"h+", {{"density", si * N}, {"temperature", Ti}, {"AA", 1.0}, {"charge", Zi}, {"velocity", 0.0}}}}}}; + component.declareAllSpecies({"e", "h+"}); component.transform(state); // Should have calculated, but not set potential diff --git a/tests/unit/test_sheath_closure.cxx b/tests/unit/test_sheath_closure.cxx index 67fe4bf6d..9093791a0 100644 --- a/tests/unit/test_sheath_closure.cxx +++ b/tests/unit/test_sheath_closure.cxx @@ -38,6 +38,7 @@ TEST_F(SheathClosureTest, NeedsDensity) { state["fields"]["phi"] = Field3D(2.0); // Needs electron density + component.declareAllSpecies({"e"}); ASSERT_THROW(component.transform(state), BoutException); } @@ -51,6 +52,7 @@ TEST_F(SheathClosureTest, PhiAndDensity) { Options state; state["fields"]["phi"] = Field3D(2.0); state["species"]["e"]["density"] = Field3D(1.5); + component.declareAllSpecies({"e"}); component.transform(state); ASSERT_TRUE(state["fields"].isSet("DivJextra")); @@ -68,7 +70,8 @@ TEST_F(SheathClosureTest, Temperature) { state["fields"]["phi"] = Field3D(2.0); state["species"]["e"]["density"] = Field3D(1.5); state["species"]["e"]["temperature"] = Field3D(1.2); - + + component.declareAllSpecies({"e"}); component.transform(state); ASSERT_TRUE(state["fields"].isSet("DivJextra")); diff --git a/tests/unit/test_snb_conduction.cxx b/tests/unit/test_snb_conduction.cxx index d79f7f000..aee2cb155 100644 --- a/tests/unit/test_snb_conduction.cxx +++ b/tests/unit/test_snb_conduction.cxx @@ -21,12 +21,16 @@ using namespace bout::globals; using SNBConductionTest = FakeMeshFixture; TEST_F(SNBConductionTest, CreateComponent) { - Options options; + Options options{ + {"units", + {{"meters", 1.0}, {"eV", 1.0}, {"inv_meters_cubed", 1e19}, {"seconds", 1e-6}}}}; SNBConduction component("test", options, nullptr); } TEST_F(SNBConductionTest, Transform) { - Options options; + Options options{ + {"units", + {{"meters", 1.0}, {"eV", 1.0}, {"inv_meters_cubed", 1e19}, {"seconds", 1e-6}}}}; SNBConduction component("test", options, nullptr); Options state{ @@ -44,7 +48,9 @@ TEST_F(SNBConductionTest, Transform) { } TEST_F(SNBConductionTest, OutputDiagnose) { - Options options; + Options options{ + {"units", + {{"meters", 1.0}, {"eV", 1.0}, {"inv_meters_cubed", 1e19}, {"seconds", 1e-6}}}}; options["test"]["diagnose"] = true; SNBConduction component("test", options, nullptr); diff --git a/tests/unit/test_sound_speed.cxx b/tests/unit/test_sound_speed.cxx index b0819e0b8..45bac22e8 100644 --- a/tests/unit/test_sound_speed.cxx +++ b/tests/unit/test_sound_speed.cxx @@ -32,7 +32,8 @@ TEST_F(SoundSpeedTest, OneSpecies) { options["species"]["e"]["density"] = 2.0; options["species"]["e"]["pressure"] = 1.2; options["species"]["e"]["AA"] = 1.5; - + + component.declareAllSpecies({"e"}); component.transform(options); ASSERT_TRUE(options.isSet("sound_speed")); @@ -51,7 +52,8 @@ TEST_F(SoundSpeedTest, TwoSpecies) { options["species"]["h"]["density"] = 3.0; options["species"]["h"]["pressure"] = 2.5; options["species"]["h"]["AA"] = 0.9; - + + component.declareAllSpecies({"e", "h"}); component.transform(options); ASSERT_TRUE(options.isSet("sound_speed")); diff --git a/tests/unit/test_zero_current.cxx b/tests/unit/test_zero_current.cxx index 4738842db..a4aea7fa0 100644 --- a/tests/unit/test_zero_current.cxx +++ b/tests/unit/test_zero_current.cxx @@ -45,6 +45,7 @@ TEST_F(ZeroCurrentTest, ElectronFlowVelocity) { Field3D Vi = FieldFactory::get()->create3D("y - x", &options, mesh); options["species"]["ion"]["velocity"] = Vi; + component.declareAllSpecies({"e", "ion"}); component.transform(options); // Electron velocity should be equal to ion velocity