This section explains how one can build tables such that they are extendable in the future. It is presented in the case of the example project.
In science projects it is usually hard if not impossible to plan out each step at the beginning of a project. Thus it is important to stay flexible enough to incorporate unexpected changes -- which, on first thought, is not along the notions of using relatively fixed tables.
Suppose you are in the scenario where you want to extend the previously coded up ContactHamiltoninan
.
If it makes sense, you can add new columns and decide how previous entries in the tables, which where inserted without this new columns in mind, add default values.
Sometimes this is not enough -- it would not make sense to adjust existing tables such that all new changes are present.
As an example, you would like to include a new Hamiltonian, e.g., corresponding to Coulomb interactions which are conceptually completely different from the ContactHamiltoninan
.
But still you are interested in eigenvalue solutions which connection is hard coded to the ContactHamiltoninan
by the hamiltonian
foreign key.
A possible solution would be to code up the new CoulombHamiltoninan
and introduce a new CoulombEigenvalue
which hamiltonian
foreign key points to the CoulombHamiltoninan
.
class Coulomb(Base):
...
class CoulombEigenvalue(Base):
hamiltonian = models.ForeignKey(Coulomb, on_delete=models.CASCADE)
...
However, this now means that we have two Eigenvalue
classes which represent the same thing and their only difference is the hamiltonian they point to
Eigenvalue -> Contact
CoulombEigenvalue -> Coulomb
A nicer solution would be if the eigenvalue point to a common base class, e.g., a Hamiltonian which is either a Coulomb
or Contact
Hamiltonian
Eigenvalue -> Hamiltonian <-> Contact
<-> Coulomb
This way, you will always have one eigenvalue class which generalizes to all ideas of an Hamiltonian class.
Warning
In this section we want to present the general idea for more complex tables.
It is in general difficult to completely reshape existing tables and therefore one should plan ahead!
To test the changes for this project, we recommend starting the database from scratch (e.g., remove the .sqlite
file and my_project/hamiltonian/migrations
files)
Django already provides a framework for implementing such common base tables using inheritance. E.g., the most minimal setup for such a scenario would be
class Hamiltonian(Base):
pass
class Contact(Hamiltonian):
n_sites = models.IntegerField()
spacing = models.DecimalField(max_digits=5, decimal_places=3)
c = models.DecimalField(max_digits=5, decimal_places=3,)
class Meta:
unique_together = ["n_sites", "spacing", "c"]
class Coulomb(Hamiltonian):
n_sites = models.IntegerField()
spacing = models.DecimalField(max_digits=5, decimal_places=3)
v = models.DecimalField( max_digits=5, decimal_places=3)
class Meta:
unique_together = ["n_sites", "spacing", "v"]
class Eigenvalue(Base):
hamiltonian = models.ForeignKey(Hamiltonian, on_delete=models.CASCADE)
n_level = models.PositiveIntegerField()
value = models.FloatField()
class Meta:
unique_together = ["hamiltonian", "n_level"]
The Eigenvalue
class now points to the Hamiltonian
table and the Contact
and Coulomb
Hamiltonian classes now inherit from Hamiltonian
.
The new classes can be used as before, e.g.,
h1 = Contact.objects.create(n_sites=10, spacing=0.1, c=-1)
h2 = Coulomb.objects.create(n_sites=10, spacing=0.1, v=20)
e1 = Eigenvalue.objects.create(hamiltonian=h1, n_level=1, value=-363.823)
e2 = Eigenvalue.objects.create(hamiltonian=h2, n_level=1, value=234.567)
Different to the base table, the Hamiltonian
table is not abstract and thus will actually be created.
E.g., these models will create the following tables after migrating
id | last_modified | tag | userid |
---|---|---|---|
1 | ... | ... | ... |
2 ... | ... | ... | |
... | ... | ... | ... |
42 | ... | ... | ... |
... | ... | ... | ... |
The id
column is the primary key to identify a certain entry.
All the other columns come from the EspressoDB Base
class (which does not have it's own table) to enable EspressoDB's features and have additional meta information.
hamiltonian_ptr_id | n_sites | spacing | c |
---|---|---|---|
1 | 10 | 0.1 | -1 |
2 | 15 | 0.1 | -1 |
... | ... | ... | ... |
The specialized hamiltonian_contact
table has no own id
.
It uses the id
column of the hamiltonian_hamiltonian
table using the hamiltonian_ptr_id
.
The other entries are specific to the actual implementation.
hamiltonian_ptr_id | n_sites | spacing | v |
---|---|---|---|
42 | 10 | 0.1 | -02 |
... | ... | ... | ... |
Similarly, the hamiltonian_coulomb
borrows it's primary key from the hamiltonian_hamiltonian
table and adds information specific to it in it's own table.
Thus, all implementations have a corresponding entry in the base hamiltonian_hamiltonian
table but specific information in their own table.
id | last_modified | tag | n_level | value | hamiltonian_id | userid |
---|---|---|---|---|---|---|
1 | ... | ... | 1 | -363.823 | 1 | ... |
1 | ... | ... | 2 | -361.803 | 1 | ... |
... | ... | ... | ... | ... | ... |
Because the hamiltonian_eigenvalue
table inherits from Base
, it comes with the default Base
columns.
In addition, it now points to the hamiltonian_id
in the hamiltonian_hamiltonian
table which corresponds to either a specialized Coulomb
or Contact
entry.
Because both the Contact
and Coulomb
table have information about n_sites
and spacing
, it would actually be possible to move these information to the base Hamiltonian
table.
This is generally possible and might also be good practice depending on the specific situation.
However, in case there are joined unique constraints, it might not always be possible because this constraint is enforced at the table level.
Suppose you want all the Contact
entries to be unique in ["n_sites", "spacing", "c"]
.
If you place the additional columns n_sites
and spacing
from Contact
to Hamiltonian
and add an unique constraint in Hamiltonian
according to ["n_sites", "spacing"]
,
class Hamiltonian(Base):
n_sites = models.IntegerField()
spacing = models.DecimalField(max_digits=5, decimal_places=3)
class Meta:
unique_together = ["n_sites", "spacing"]
class Contact(Hamiltonian):
c = models.DecimalField(max_digits=5, decimal_places=3)
class Meta:
unique_together = ["hamiltonian_ptr_id", "c"]
it is not possible to have table entries for same n_site
and spacing
but different c
,
id | n_sites | spacing | ... |
---|---|---|---|
1 | 10 | 0.1 | ... |
... | ... | ... | ... |
hamiltonian_ptr_id | c |
---|---|
1 | -1.0 |
1 | -2.0 (this is not possible) |
because each entry in hamiltonian_contact
creates a new id
in hamiltonian_hamiltonian
which is unique constrained in the parameters we want to have present.
In principle one could unique constrain ["id", "n_sites", "spacing"]
in hamiltonian_hamiltonian
, however unique constraining any combination of columns containing the id
is equivalent to not constraining at all (because the id
is supposed to be unique).
In case of inheritance, queries and member access changes slightly.
E.g., if one wants to look up the corresponding Contact
Hamiltonian of eigenvalues, one would have to use the following code
h = Eigenvalue.objects.filter(hamiltonian__contact__c=-1.0).first()
Or on the python level
e1 = Eigenvalue.objects.first()
h = e1.hamiltonian.contact # potentially none if hamiltonian not of type contact
h.c == -1.0
Note that this access might fail if the Hamiltonian is a Coulomb
Hamiltonian.
To be save against this, EspressoDB provides the specialization
attribute which identifies the type of the instance by it's primary key, e.g.,
h0 = e1.hamiltonian.specialization
h0 == h
Furthermore, to avoid redundancy, EspressoDB provides convenience methods to circumvent the access of the specialization attribute. E.g., it is possible to use the syntax
e1 = Eigenvalue.objects.first()
h = e1.hamiltonian # no extra access to .contact
h.c == -1.0 # only present if h2 is of type contact, else it is .v
Note
Note that h
is still an instance of Hamiltonian
, it just loads in all the members of Contact
.
When you change the members belonging to Contact
, and call save
, also the corresponding save of Contact
is called.