Settings Attribute Machinery#

Each object has a settings attribute which is used to store relevant and useful data. This is particularly useful for the algorithms where things like solver tolerance can be set. This notebook will given an overview of this attribute and its features and behavior.

import openpnm as op

Let’s start by creating a StokesFlow algorithm, which has plenty of settings so is good to demonstrate:

pn = op.network.Demo(shape=[4, 4, 1])
w = op.phase.Water(network=pn)
w.add_model_collection(op.models.collections.physics.basic)
flow = op.algorithms.StokesFlow(network=pn, phase=w)

First lets print the settings attribute:

print(flow.settings)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Settings                            Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
uuid                                3d5421d2-13f1-4ad1-b632-8e7000d9d5bf
default_domain                      domain_1
cache                               True
conductance                         throat.hydraulic_conductance
phase                               phase_01
quantity                            pore.pressure
variable_props                      TypedSet()
f_rtol                              1e-06
newton_maxiter                      5000
relaxation_factor                   1.0
x_rtol                              1e-06
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

settings is a custom class that behaves somewhat like Python’s dataclass but is actually useful (sorry Python). Let’s start by exploring the settings on flow (without talking about their meaning).

Datatype is enforced#

Once a data type is written to an attribute, all subsequent values must be of the same type.

flow.settings.f_rtol = 2.0
print(flow.settings.f_rtol)
2.0
try:
    flow.settings.f_rtol = 'two'
except Exception as e:
    print(e)
Attribute 'f_rtol' can only accept values of type <class 'float'>, but the recieved value was of type <class 'str'>

Settings can be access as attributes or dict keys#

This feature just makes it easy to access things programmatically using flow.settings[key] rather then getattr(flow.settings, key), while ensuring the settings attributes can still be easily viewed using tab-completion.

print(flow.settings['f_rtol'])
flow.settings['f_rtol'] = 3.0
print(flow.settings.f_rtol)
2.0
3.0

Namespace is clean#

When developing this feature we considered many relevant packages like traits, attrs, pydantic, etc, but they all had a major drawback: The namespace the class was cluttered with many methods relevant to using the package. We wanted the settings attribute to contain only settings:

for item in dir(flow.settings):
    if not item.startswith('_'):
        print(item)
cache
conductance
default_domain
f_rtol
newton_maxiter
phase
quantity
relaxation_factor
uuid
variable_props
x_rtol

Collections also enforce types#

As shown above, once a setting is given a certain value, all future values must have the same datatype. We also implemented several “typed” collections, like TypedList and TypedSet, which insist that all elements must have the same type:

print(flow.settings.variable_props)
TypedSet()
flow.settings.variable_props.add('pore.pressure')
print(flow.settings.variable_props)
TypedSet({'pore.pressure'})
try:
    flow.settings.variable_props.add(0.0)
except Exception as e:
    print(e)
This list cannot accept values of type <class 'float'>

Note that the first entry into the TypedSet defines the type of all subsequent entries. The reason for this type enforcement is basically to prevent users from writing a value to a setting that OpenPNM does not expect. OpenPNM fetches these values from various places in the code and uses them, so they must be usable.

Settings are attached before init#

An instance of the Settings class is attached to the settings attribute of every object even prior to initialization. This is done by overloading the __new__ method of the Base2 class, from which every OpenPNM object descends.

new = op.core.Base2()
print(new.settings)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Settings                            Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
uuid                                f7a96033-dba8-47b9-acb9-2b40307769d8
default_domain                      domain_1
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

During this step the uuid is generated, to ensure all objects have a unique value here (This is useful if an object is saved and reloaded for instance).