Skip to content

Commit 053c9bb

Browse files
authored
Merge pull request #421 from boutproject/cmacmackin/access_control
Control access to state variables in transform method
2 parents 4cbf2d5 + a2ac1e8 commit 053c9bb

File tree

134 files changed

+3891
-1064
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

134 files changed

+3891
-1064
lines changed

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ set(HERMES_SOURCES
7373
src/evolve_energy.cxx
7474
src/evolve_pressure.cxx
7575
src/evolve_momentum.cxx
76+
src/guarded_options.cxx
7677
src/isothermal.cxx
7778
src/quasineutral.cxx
7879
src/diamagnetic_drift.cxx
@@ -93,6 +94,7 @@ set(HERMES_SOURCES
9394
src/noflow_boundary.cxx
9495
src/neutral_parallel_diffusion.cxx
9596
src/neutral_boundary.cxx
97+
src/permissions.cxx
9698
src/polarisation_drift.cxx
9799
src/solkit_neutral_parallel_diffusion.cxx
98100
src/hydrogen_charge_exchange.cxx
@@ -133,6 +135,7 @@ set(HERMES_SOURCES
133135
include/fixed_density.hxx
134136
include/fixed_fraction_ions.hxx
135137
include/fixed_velocity.hxx
138+
include/guarded_options.hxx
136139
include/neutral_full_velocity.hxx
137140
include/hermes_utils.hxx
138141
include/hydrogen_charge_exchange.hxx
@@ -144,6 +147,7 @@ set(HERMES_SOURCES
144147
include/neutral_parallel_diffusion.hxx
145148
include/solkit_neutral_parallel_diffusion.hxx
146149
include/noflow_boundary.hxx
150+
include/permissions.hxx
147151
include/polarisation_drift.hxx
148152
include/quasineutral.hxx
149153
include/reaction.hxx

docs/sphinx/closure.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ Input
262262

263263
This top-level component calculates the frictional forces between each
264264
pair of species for which collisional frequencies have been calculated
265-
(see :ref:`Braginskii Collisions`). As such, it must be run after
265+
(see `Braginskii Collisions component`_). As such, it must be run after
266266
`braginskii_collisions`. If the option `frictional_heating` is
267267
enabled then it will also calculate the energy source arising from
268268
friction.
@@ -346,7 +346,7 @@ Braginskii Heat Exchange
346346
Input
347347
-----
348348
This top-level component calculates the heat exchange between species
349-
due to collisions (see :ref:`Braginskii Collisions`). As such, it must be run after
349+
due to collisions (see `Braginskii Collisions component`_). As such, it must be run after
350350
`braginskii_collisions`. There are no configurations for this component.
351351

352352
Theory

docs/sphinx/developer.rst

Lines changed: 246 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,9 @@ are of unit length. See relevant `BOUT++ docs
181181
<https://bout-dev.readthedocs.io/en/stable/developer_docs/data_types.html>`_
182182
for more info. There is also a data type called ``Options`` which is equivalent
183183
to a Python dictionary with extra functionality, and is used to store input
184-
options, the entire simulation state and many other data.
184+
options, the entire simulation state and many other data. Finally,
185+
there is the ``GuardedOptions`` datatype, which wraps an ``Options``
186+
object and controls access to its contents.
185187

186188

187189
Adding new settings
@@ -265,7 +267,7 @@ the variables in one place, which could allow some components to overwrite other
265267
In ``component.hxx`` there is the function ``get``, which once called sets the
266268
"final" and "final-domain" attributes:
267269

268-
.. code-bloc:: ini
270+
.. code-block:: ini
269271
270272
T get(const Options& option, const std::string& location = "") {
271273
#if CHECKLEVEL >= 1
@@ -328,6 +330,9 @@ And there is a corresponding ``setBoundary`` that can be used for BC operations:
328330
return option;
329331
}
330332
333+
All of these functions are overloaded to accept both `Options` and
334+
`GuardedOptions` objects.
335+
331336
These functions take a second argument which tells you where they were set, which is easier for debugging.
332337
They are wrapped into additional functions, ``GET_VALUE`` and ``GET_NOBOUNDARY`` which automatically
333338
include this argument.
@@ -516,21 +521,28 @@ Notes:
516521
- The species name convention is that the charge state is last, after the `+` or `-`
517522
sign: `n2+` is a singly charged nitrogen molecule, while `n+2` is a +2 charged
518523
nitrogen atom.
524+
519525

520526
Components
521527
~~~~~~~~~~~~~~
522528

523529
The basic building block of all Hermes-3 models is the
524530
`Component`. This defines an interface to a class which takes a state
525-
(a tree of dictionaries/maps), and transforms (modifies) it. After
526-
all components have modified the state in turn, all components may
527-
then implement a `finally` method to take the final state but not
531+
(a tree of dictionaries/maps) and transforms (modifies) it. This is
532+
done by calling the public `Component::transform` method. This will
533+
call the private `Component::transform_impl` method, which must be
534+
overriden for each Component implementation.
535+
536+
After all components have modified the state in turn, all components
537+
may then implement a `finally` method to take the final state but not
528538
modify it. This allows two components to depend on each other, but
529539
makes debugging and testing easier by limiting the places where the
530540
state can be modified.
531541

532542
.. doxygenstruct:: Component
533543
:members:
544+
:protected-members:
545+
:private-members:
534546

535547
Components are usually defined in separate files; sometimes multiple
536548
components in one file if they are small and related to each other (e.g.
@@ -552,7 +564,7 @@ file using a code like::
552564
where `MyComponent` is the component class, and "mycomponent" is the
553565
name that can be used in the BOUT.inp settings file to create a
554566
component of this type. Note that the name can be any string except it
555-
can't contain commas or brackets (), and shouldn't start or end with
567+
can't contain commas or brackets, and shouldn't start or end with
556568
whitespace.
557569

558570
Inputs to the component constructors are:
@@ -565,12 +577,40 @@ The `name` is a string labelling the instance. The `alloptions` tree contains at
565577

566578
* `alloptions[name]` options for this instance
567579
* `alloptions['units']`
568-
580+
581+
582+
Component Permissions
583+
`````````````````````
584+
585+
All component constructors must pass a `Permissions` object (see
586+
below) to the constructor on the `Component::Component` base
587+
class. This specifies which variables will be read/written by the
588+
`Component::transform` method and will be used to construct a
589+
`GuardedOptions` object to be passed into
590+
`Component::transform_impl`. The `Permissions` object can be further
591+
updated in the body of the constructor of your component using the
592+
`Component::setPermissions` and `Component::substitutePermissions`
593+
methods. You should give read and write permissions to the minimum
594+
number of variables necessary, to avoid circular dependencies arising
595+
among components.
596+
597+
A number of substitutions will automatically be performed on your
598+
permissions (see `Permission Substitution`_), so that you can specify
599+
permissions for some variables for each species. For example, the
600+
following permissions would give read access to pressure for all
601+
species and density of ions::
602+
603+
MyComponent::MyComponent(const std::string &name, Options &options,
604+
Solver *solver) : Component({readOnly("species:{all_species}:pressure"),
605+
readOnly("species:{ions}:density")}) {}
606+
607+
See the documentation for `Component::declareAllSpecies` for a list of
608+
all substitutions that will be performed.
609+
569610

570611
Component scheduler
571612
~~~~~~~~~~~~~~
572613

573-
574614
The simulation model is created in `Hermes::init` by a call to the `ComponentScheduler`::
575615

576616
scheduler = ComponentScheduler::create(options, Options::root(), solver);
@@ -599,7 +639,7 @@ scheduler looks up the options under the section of that name.
599639
600640
This would create two `Component` objects, of type `component1` and
601641
`component2`. Each time `Hermes::rhs` is run, the `transform`
602-
functions of `component1` amd then `component2` will be called,
642+
functions of `component1` and then `component2` will be called,
603643
followed by their `finally` functions.
604644

605645
It is often useful to group components together, for example to
@@ -629,6 +669,202 @@ in `group1`, and then `component3`.
629669
:members:
630670

631671

672+
Permissions
673+
~~~~~~~~~~~~~~
674+
675+
The ``Permissions`` class can be used to store information about which
676+
variables within an ``Options`` object are allowed to be accessed and
677+
for what purpose. This is used to control the variables used by a
678+
``Component``. There is a hierarchy of four types of increasing
679+
permission. These are expressed using the
680+
`PermissionTypes` `enum <https://en.wikipedia.org/wiki/Enumerated_type>`__:
681+
682+
#. **ReadIfSet:** Only allowed to read variable if it is already set.
683+
#. **Read:** Can read the contents of the variable. Assumes it has already been set.
684+
#. **Write:** Can write variable. Makes no assumption about whether it has already been written or will be written again in future.
685+
#. **Final:** This will be the last component to write to the variable. Only one component may have ``Final`` permission for a given variable.
686+
687+
The order these per permissions are listed in is significant: each
688+
higher permission implies a component also has all lower permissions. E.g.,
689+
write permission implies read permission as well.
690+
691+
Declaring Permissions for Particular Variables
692+
``````````````````````````````````````````````
693+
694+
Permission information for a variable is stored in a
695+
`Permissions::VarRights` object. The overwhelming majority of the
696+
permissions you would want to create can be constructed using one of
697+
the provided convenience-functions. For example::
698+
699+
Permissions::VarRights read_e_pressure = readOnly("species:e:pressure");
700+
Permissions::VarRights write_d_density = readWrite("species:d:density");
701+
Permissions::VarRights read_e_velocity_in_interior_if_set =
702+
readIfSet("species:e:velocity", Regions::Interior);
703+
704+
Permissions can be set to apply only to a particular region of the
705+
domain (e.g., the boundary or the interior) using a `Regions` enum
706+
(see `Specifying a Region`_).
707+
708+
Creating Permissions Objects
709+
````````````````````````````
710+
711+
Permission data like that created in the previous example can be used
712+
to construct a ``Permissions`` object. These objects describe the
713+
permissions for multiple variables.::
714+
715+
Permissions p({readOnly("time"),
716+
readOnly("species:e:pressure"),
717+
readWrite("species:e:momentum", Regions::Interior)});
718+
719+
A permission applied to a section of an ``Options`` object will apply
720+
to all variables contained within that section, unless a more specific
721+
permission is also set. Therefore, if we have a state with variables
722+
``species:e:pressure``, ``species:e:density``, ``species:e:velocity``,
723+
and ``species:e:momentum``, then the following are equivalent::
724+
725+
Permissions p({readOnly("species:e"),
726+
readWrite("species:e:momentum")});
727+
Permissions p({readOnly("species:e:pressure"),
728+
readOnly("species:e:density"),
729+
readOnly("species:e:velocity"),
730+
readWrite("species:e:momentum")});
731+
732+
Specifying a Region
733+
```````````````````
734+
735+
The `PermissionTypes` are applied to particular regions of the domain.
736+
This allows, e.g., for there to be read permissions for the interior
737+
of the domain but write permissions for the boundaries. Regions are
738+
expressed using the `Permissions::Regions` enum, which functions as a `bitset
739+
<https://en.wikipedia.org/wiki/Bit_array>`__. You can combine regions
740+
using bitwise logical operators.
741+
742+
.. doxygengroup:: RegionsGroup
743+
:members:
744+
745+
Permission Substitution
746+
```````````````````````
747+
748+
Variable names can include labels, marked in curly-braces, that will
749+
later be substituted (using `Permissions::substitute` and
750+
`Component::substitutePermissions`). Substitutions are necessary
751+
because, when declaring permissions for a `Component`, you may need to
752+
express that it can access some variable for all species (or all ions,
753+
all neutrals, etc.), but you won't yet know the names of all the
754+
species. For example, if you need to read the density of all species
755+
and write the collision frequency of all ions then you would write::
756+
757+
Permissions p({readOnly("species:{all_spcies}:density"),
758+
readWrite("species:{ions}:collision_frequency"});
759+
760+
If there are species e, d, d+, h, and h+ then the above will be
761+
equivalent to::
762+
763+
Permissions p({readOnly("species:e:density"),
764+
readOnly("species:d:density"),
765+
readOnly("species:d+:density"),
766+
readOnly("species:h:density"),
767+
readOnly("species:h+:density"),
768+
readWrite("species:d+:collision_frequency"),
769+
readWrite("species:h+:collision_frequency")});
770+
771+
These substitutions will be performed in
772+
`Component::declareAllSpecies`. See the documentation for that method
773+
for a full list of the substitutions which it can perform.
774+
775+
It can also be useful to define your own substitutions, to save
776+
repetitive declarations. For example, you could declare read
777+
permissions for electron density, pressure, temperature, velocity, and
778+
momentum as follows::
779+
780+
Permissions p({readOnly("species:e:{inputs}");
781+
p.substitute({"density", "pressure", "temperature", "velocity", "momentum"});
782+
783+
This is equivalent to having written::
784+
785+
Permissions p({readOnly("species:e:density"),
786+
readOnly("species:e:pressure")},
787+
readOnly("species:e:temperature")},
788+
readOnly("species:e:velocity")},
789+
readOnly("species:e:momentum")});
790+
791+
Permission Factory Functions
792+
````````````````````````````
793+
794+
.. doxygengroup:: PermissionFactories
795+
:members:
796+
797+
Permissions Class
798+
`````````````````
799+
.. doxygenclass:: Permissions
800+
:members:
801+
802+
Further Implementation Details
803+
``````````````````````````````
804+
805+
The above information should be sufficient for users that are
806+
developing or modifying components. The following explains in more
807+
detail how permission data is stored and should be read by anyone
808+
looking to modify the `Permissions` or `GuardedOptions` classes.
809+
810+
Permission information for a variable gets stored in
811+
`Permissions::AccessRights` objects, which are arrays of
812+
`Regions`. Each element of the array corresponds to information about
813+
a permission level: ``{read_if_set, read, write, final}``. To access
814+
the element for a desired permission level, you can index the array
815+
with the corresponding member of the `PermissionTypes` enum::
816+
817+
Permissions::AccessRights rights;
818+
Regions read_regions = rights[PermissionTypes::Read];
819+
Regions write_regions = rights[PermissionTypes::Write];
820+
821+
The contents of each element of an `Permissions::AccessRights` array
822+
is the set of regions for which the permissions apply. For example::
823+
824+
Permissions::AccessRights read_boundaries_if_set =
825+
{Regions::Boundaries, Regions::Nowhere, Regions::Nowhere,
826+
Regions::Nowhere};
827+
Permissions::AccessRights read_interior_write_boundaries =
828+
{Regions::Nowhere, Regions::Interior, Regions::Boundaries,
829+
Regions::Nowhere};
830+
Permissions::AccessRights final_write_all_regions =
831+
{Regions::Nowhere, Regions::Nowhere, Regions::Nowhere, Regions::All};
832+
833+
The `Permissions::VarRights` struct is used to pair a variable name
834+
with a `Permissions::AccessRights` array containing the permission
835+
information for that variable.
836+
837+
838+
GuardedOptions
839+
~~~~~~~~~~~~~~
840+
841+
``GuardedOptions`` objects combine a `Permissions` object and an
842+
`Options` object. They can be indexed just like normal ``Options``
843+
objects but will return another ``GuardedOptions``, wrapping the
844+
result. In order to read or write the contents of a ``GuardedOptions``
845+
object you must use the ``get()`` or ``getWritable()`` methods,
846+
respectively. These will return the underlying (const) ``Options``
847+
object, if you have the necessary permissions to access it. Otherwise,
848+
they will raise an exception.
849+
850+
If ``CHECKLEVEL`` is 1 or above, then the ``GuardedOptions`` will track
851+
which variables have actually been accessed. Lists of
852+
unread/unwritten variables can be returned with the ``unreadItems()``
853+
and ``unwrittenItems()`` methods. If ``CHECKLEVEL`` is zero then
854+
calling these methods will raise an exception.
855+
856+
.. doxygenclass:: GuardedOptions
857+
:members:
858+
859+
.. note::
860+
When indexing a ``GuardedOptions`` object, it will create a new
861+
``GuardedOptions`` on-demand. This is unlike with a normal
862+
``Options`` object which returns a reference to a preexisting child
863+
``Options`` object. You generally should not store
864+
``GuardedOptions`` by reference. You may be able to pass them by
865+
reference, but this requires you to think carefully about whether
866+
the argument is going to be an r-value or an l-value.
867+
632868
.. _sec-tests:
633869

634870
Tests
@@ -893,4 +1129,4 @@ There are two simple integrated tests to make sure that the collision frequency
8931129
across `neutral_mixed`, `evolve_pressure`, `ion_viscosity` and `neutral_parallel_diffusion`.
8941130
A minimal 3D geometry is run for one RHS evaluation, and the test checks the log file
8951131
to make sure the correct collisionalities were selected. One of the tests is for the `multispecies`
896-
mode across all components, while the other is for `braginskii` for plasma and `afn` for neutrals.
1132+
mode across all components, while the other is for `braginskii` for plasma and `afn` for neutrals.

0 commit comments

Comments
 (0)