Association Rules¶
Association definitions, or rules
, are Python classes, all based on
Association
. The base class provides only a
framework, much like an abstract base class; all functionality must be
implemented in sub-classes.
Any subclass that is intended to produce an association is referred to
as a rule
. Any rule subclass must have a name that begins with the
string Asn_
. This is to ensure that any other classes involved in
defining the definition of the rule classes do not get used as rules
themselves, such as the Association
itself.
Association Dynamic Definition¶
Associations are created by matching members to rules. However, an important concept to remember is that an association is defined by both the rule matched, and by the initial member that matched it. The following example will illustrate this concept.
For JWST Level 3, many associations created must have members that all share the same filter. To avoid writing rules for each filter, the rules have a condition that states that it doesn’t matter what filter is specified, as long as the association contains all the same filter.
To accomplish this, the association defines a constraint where filter must have a valid value, but can be any valid value. When the association is first attempted to be instantiated with a member, and that member has a valid filter, the association is created. However, the constraint on filter value in the newly created association is modified to match exactly the filter value that the first member had. Now, when other members are attempted to be added to the association, the filter of the new members must match exactly with what the association is expecting.
This dynamic definition allows rules to be written where each value of a specific attribute of a member does not have to be explicitly stated. This provides for very robust, yet concise, set of rule definitions.
User-level API¶
Core Keys¶
To be repetitive, the basic association is simply a dict (default) or list. The structure of the dict is completely determined by the rules. However, the base class defines the following keys:
asn_type
The type of the association.
asn_rule
The name of the rule.
version_id
A version number for any associations created by this rule.
code_version
The version of the generator library in use.
These keys are accessed in the same way any dict key is accessed:
asn = Asn_MyAssociation()
print(asn['asn_rule'])
#--> MyAssociation
Core Methods¶
These are the methods of an association rule deal with creation or returning the created association. A rule may define other methods, but the following are required to be implemented.
create()
Create an association.
add()
Add a member to the current association.
dump()
Return the string serialization of the association.
load()
Return the association from its serialization.
Creation¶
To create an association based on a member, the create
method of the
rule is called:
(association, reprocess_list) = Asn_SomeRule.create(member)
create
returns a 2-tuple: The first element is the association and the
second element is a list of reprocess
instances.
If the member matches the conditions for the rule, an association is
returned. If the member does not belong, None
is returned for the
association.
Whether an association is created or not, it is possible a list of
reprocess
instances may be returned. This list represents the
expansion of the pool in Member Attributes that are Lists
Addition¶
To add members to an existing association, one uses the Association.add
method:
(matches, reprocess_list) = association.add(new_member)
If the association accepts the member, the matches
element of the
2-tuple will be True
.
Typically, one does not deal with a single rule, but a collection of
rules. For association creation, one typically uses an
AssociationRegistry
to collect all the rules a pool will be
compared against. Association registries provide extra functionality to
deal with a large and varied set of association rules.
Saving and Loading¶
Once created, an association can be serialized using its
Association.dump
method.
Serialization creates a string representation of the association which
can then be saved as one wishes. Some code that does a basic save
looks like:
file_name, serialized = association.dump()
with open(file_name, 'w') as file_handle:
file_handle.write(serialized)
Note that dump
returns a 2-tuple. The first element is the suggested
file name to use to save the association. The second element is the
serialization.
To retrieve an association, one uses the Association.load
method:
with open(file_name, 'r') as file_handle:
association = Association.load(file_handle)
Association.load
will only validate
the incoming data against whatever schema or other validation checks
the particular subclass calls for. The generally preferred method for
loading an association is through the
jwst.associations.load_asn()
function.
Defining New Associations¶
All association rules are based on the
Association
base class. This
class will not create associations on its own; subclasses must be
defined. What an association is and how it is later used is completely
left to the subclasses. The base class itself only defines the
framework required to create associations. The rest of this section
will discuss the minimum functionality that a subclass needs to
implement in order to create an association.
Class Naming¶
The AssociationRegistry
is used to store
the association rules. Since rules are defined by Python classes, a
way of indicating what the final rule classes are is needed. By
definition, rule classes are classes that begin with the string Asn_
.
Only these classes are used to produce associations.
Core Attributes¶
Since rule classes will potentially have a large number of attributes
and methods, the base Association
class defines two
attributes: data
, which contains the actual association, and meta
,
the structure that holds auxiliary information needed for association
creation. Subclasses may redefine these attributes as they see fit.
However, it is suggested that they be used as conceptually defined here.
data
Attribute¶
data
contains the association itself. Currently, the base class
predefines data
as a dict. The base class itself is a subclass of
MutableMapping
. Any instance behaves as a dict. The contents of that
dict is the contents of the data
attribute. For example:
asn = Asn_MyAssociation()
asn.data['value'] = 'a value'
assert asn['value'] == 'a value'
# True
asn['value'] = 'another value'
assert asn.data['value'] == 'another value'
# True
Instantiation¶
Instantiating a rule, in and of itself, does nothing more than setup the constraints that define the rule, and basic structure initialization.
Implementing create()
¶
The base class function performs the following steps:
Instantiates an instance of the rule
Calls
add()
to attempt to add the member to the instance
If add()
returns matches==False
, then create
returns None
as the
new association.
Any override of this method is expected to first call super
. On
success, any further initialization may be performed.
Implementing add()
¶
The add()
method adds
members to an association.
If a member does belong to the association, the following events occur:
- Constraint Modification
Any wildcard constraints are modified so that any further matching must match exactly the value provided by the current member.
self._init_hook()
is executedIf a new association is being created, the rule’s
_init_hook
method is executed, if defined. This allows a rule to do further initialization before the member is officially added to the association.self._add()
is executedThe rule class must define
_add()
. This method officially adds the member to the association.
Implementing dump()
and load()
¶
The base Association
class defines the
dump()
and
load()
methods to
serialize the data structure pointing to by the data
attribute. If
the new rule uses the data
attribute for storing the association
information, no further overriding of these methods is necessary.
However, if the new rule does not define data
, then these methods
will need be overridden.
Rule Registration¶
In order for a rule to be used by generate
, the rule must be loaded
into an AssociationRegistry
. Since a rule is just a class that is
defined as part of a, most likely, larger module, the registry needs
to know what classes are rules. Classes to be used as rules are marked
with the RegistryMarker.rule
decorator as follows:
# myrules.py
from jwst.associations import (Association, RegistryMarker)
@RegistryMarker.rule
class MyRule(Association):
...
Then, when the rule file is used to create an AssociationRegistry
,
the class MyRule
will be included as one of the available rules:
.. doctest-skip::
>>> from jwst.associations import AssociationRegistry
>>> registry = AssociationRegistry('myrules.py', include_default=False)
>>> print(registry)
{'MyRule': <class 'abc.MyRule'>}