Sunday, September 2, 2018

Implementing class based views and class hierarchy

So finally I managed to migrate the function based views in the Django code to class based views. This has resulted in significant decrease in code repetition and the code is now much more readable and maintainable.

To start, I needed to create a class hierarchy. The base class in SimulationData which extracts the simulation model instance, reads the circuit files and processes them. The next layer is ListControlVariables that inherits SimulationData and extracts control files and their variables.

The class SimulationData is:

class SimulationData:
"""
This class is a parent class to most other class views.
It extracts a simulation model, reads the circuit files,
processes circuits and checks for errors.
"""
def get_sim_model(self):
"""
Gets the simulation model instance from the
GET request parameters.
"""
if 'id' in self.kwargs:
self.sim_id = int(self.kwargs['id'])
self.sim_para_model = SimulationCase.objects.get(id=self.sim_id)
else:
self.sim_para_model = None
return
def get_circuit_files(self):
"""
Get all the circuit schematics in a simulation model.
"""
try:
self.sim_para_model
except:
self.get_sim_model()
if self.sim_para_model:
self.ckt_file_list = self.sim_para_model.circuitschematics_set.all()
else:
self.ckt_file_list = None
return
def get_circuit_read_errors(self):
"""
Check if any of the circuit schematics cannot be read.
"""
self.ckt_read_errors = []
self.ckt_errors = -1
if self.ckt_file_list:
for ckt_file_item in self.ckt_file_list:
ckt_full_path = os.path.join(os.sep, \
self.sim_para_model.sim_working_directory, \
ckt_file_item.ckt_file_name)
# Try to read the file.
try:
check_ckt_file = open(ckt_full_path, "r")
# If can't be read, it means file doesn't exist in the working directory.
except:
self.ckt_read_errors.append('Circuit spreadsheet could not be read. \
Make sure it is in same directory as working directory above')
self.ckt_errors = 1
else:
self.ckt_read_errors.append('')
return
def process_circuit_schematics(self):
"""
This function also reads circuit schematics and
generates component objects. It checks for network errors.
"""
try:
self.sim_para_model
except:
self.get_sim_model()
self.get_circuit_files()
self.nw_input = []
self.conn_ckt_mat = []
if self.ckt_file_list:
for ckt_file_item in self.ckt_file_list:
self.nw_input.append(ckt_file_item.ckt_file_name.split(".csv")[0])
full_file_path = os.path.join(os.sep, \
self.sim_para_model.sim_working_directory, \
ckt_file_item.ckt_file_name)
ckt_file_object = open(full_file_path, "r")
# Read the circuit into conn_ckt_mat
# Also performs a scrubbing of circuit spreadsheet
self.conn_ckt_mat.append(NwRdr.csv_reader(ckt_file_object))
# Making a list of the type of components in the
# circuit.
self.components_found, self.component_objects, self.ckt_error_list = \
NwRdr.determine_circuit_components(self.conn_ckt_mat, self.nw_input)
if not self.ckt_error_list:
# Make lists of nodes and branches in the circuit.
self.node_list, self.branch_map, self.node_branch_errors = \
NwRdr.determine_nodes_branches(self.conn_ckt_mat, self.nw_input)
if self.node_branch_errors:
self.ckt_error_list.extend(self.node_branch_errors)
return
def update_db_parameters(self):
"""
Checks if the components in the circuit schematics
exist in the database. If they do, their positions
are updated. If they are new, database entries are
created.
"""
try:
self.sim_para_model
except:
self.get_sim_model()
try:
self.components_found
except:
self.process_circuit_schematics()
all_components = self.sim_para_model.circuitcomponents_set.all()
for comp_types in self.components_found.keys():
# Take every type of component found
# item -> resistor, inductor etc
for c1 in range(len(self.components_found[comp_types])):
# Each component type will be occurring
# multiple times. Iterate through every find.
# The list corresponding to each component is
# the unique cell position in the spreadsheet
check_comp_exists = all_components.filter(comp_type=comp_types).\
filter(comp_tag=self.components_found[comp_types][c1][1])
if check_comp_exists and len(check_comp_exists)==1:
old_comp_object = check_comp_exists[0]
old_comp_object.comp_number = c1 + 1
old_comp_object.comp_pos_3D = self.components_found[comp_types][c1][0]
old_comp_object.comp_pos = NwRdr.csv_element_2D(\
NwRdr.csv_tuple(self.components_found[comp_types][c1][0])[1:])
sheet_number = NwRdr.csv_tuple(self.components_found[comp_types][c1][0])[0]
old_comp_object.comp_sheet = sheet_number
old_comp_object.sheet_name = self.nw_input[sheet_number] + ".csv"
old_comp_object.sim_case = self.sim_para_model
old_comp_object.save()
self.sim_para_model.save()
else:
new_comp_object = CircuitComponents()
new_comp_object.comp_type = comp_types
new_comp_object.comp_number = c1 + 1
new_comp_object.comp_pos_3D = self.components_found[comp_types][c1][0]
new_comp_object.comp_pos = NwRdr.csv_element_2D(\
NwRdr.csv_tuple(self.components_found[comp_types][c1][0])[1:])
sheet_number = NwRdr.csv_tuple(self.components_found[comp_types][c1][0])[0]
new_comp_object.comp_sheet = sheet_number
new_comp_object.sheet_name = self.nw_input[sheet_number] + ".csv"
new_comp_object.comp_tag = self.components_found[comp_types][c1][1]
new_comp_object.sim_case = self.sim_para_model
new_comp_object.save()
self.sim_para_model.save()
for comp_items in self.component_objects.keys():
self.component_objects[comp_items].create_form_values(
self.sim_para_model,
self.ckt_file_list,
self.branch_map
)
# Generate a table of meters that can be used when designing
# control interfaces.
try:
self.meter_list = self.sim_para_model.metercomponents_set.all()
except:
self.meter_list = []
for comp_items in self.component_objects.keys():
if self.component_objects[comp_items].is_meter=="yes":
meter_found = False
if self.meter_list:
check_meter = self.meter_list.\
filter(comp_type=self.component_objects[comp_items].type).\
filter(comp_tag=self.component_objects[comp_items].tag)
if check_meter and len(check_meter)==1:
old_meter_item = check_meter[0]
old_meter_item.ckt_file_name = \
self.component_objects[comp_items].sheet_name
old_meter_item.comp_pos_3D = \
self.component_objects[comp_items].pos_3D
old_meter_item.save()
self.sim_para_model.save()
meter_found = True
else:
meter_found = False
if not meter_found:
new_meter_item = models.MeterComponents()
new_meter_item.comp_type = self.component_objects[comp_items].type
new_meter_item.comp_tag = self.component_objects[comp_items].tag
new_meter_item.comp_pos_3D = self.component_objects[comp_items].pos_3D
new_meter_item.ckt_file_name = self.component_objects[comp_items].sheet_name
new_meter_item.comp_name = self.component_objects[comp_items].type + \
"_" + self.component_objects[comp_items].tag
new_meter_item.sim_case = self.sim_para_model
new_meter_item.save()
self.sim_para_model.save()
# Remove meters that have been deleted from the circuit
for meter_item in self.sim_para_model.metercomponents_set.all():
if meter_item.comp_pos_3D not in self.component_objects.keys():
meter_item.delete()
self.sim_para_model.save()
# Generate a table of control components from circuit components
# that can be used for designing control interfaces.
try:
self.control_comp_list = self.sim_para_model.controllablecomponents_set.all()
except:
self.control_comp_list = []
for comp_items in self.component_objects.keys():
if self.component_objects[comp_items].has_control=="yes":
controllable_comp_found = False
if self.control_comp_list:
check_control_comp = self.control_comp_list.\
filter(comp_type=self.component_objects[comp_items].type).\
filter(comp_tag=self.component_objects[comp_items].tag)
if check_control_comp and len(check_control_comp)==1:
old_control_item = check_control_comp[0]
old_control_item.ckt_file_name = \
self.component_objects[comp_items].sheet_name
old_control_item.comp_pos_3D = \
self.component_objects[comp_items].pos_3D
old_control_item.save()
self.sim_para_model.save()
controllable_comp_found = True
else:
controllable_comp_found = False
if not controllable_comp_found:
new_control_item = models.ControllableComponents()
new_control_item.comp_type = self.component_objects[comp_items].type
new_control_item.comp_tag = self.component_objects[comp_items].tag
new_control_item.comp_pos_3D = self.component_objects[comp_items].pos_3D
new_control_item.ckt_file_name = self.component_objects[comp_items].sheet_name
new_control_item.comp_name = self.component_objects[comp_items].type + \
"_" + self.component_objects[comp_items].tag
new_control_item.control_tag = self.component_objects[comp_items].control_tag
new_control_item.sim_case = self.sim_para_model
new_control_item.save()
self.sim_para_model.save()
# Delete any database entries that are no longer in the circuit.
for c2 in range(len(all_components)-1, -1, -1):
comp_exist = all_components[c2]
comp_found = False
c1 = 0
if comp_exist.comp_type in self.components_found.keys():
while c1<len(self.components_found[comp_exist.comp_type]) and comp_found==False:
if self.components_found[comp_exist.comp_type][c1][1]==comp_exist.comp_tag:
comp_found = True
c1 += 1
if comp_found==False:
comp_exist.delete()
self.sim_para_model.save()
return
def get_plot_data(self):
"""
Generate the list of plots.
"""
return [
self.sim_para_model.circuitplot_set.all(),
self.sim_para_model.plotlines_set.all()
]

The methods of the class perform functions like retrieving the simulation model instance, getting the circuit schematic model instances, checking if there are errors in reading the circuit files, processing the simulation circuit files and updating the database.

A few are obvious like getting the simulation model instance or circuit files model instances. Some not so obvious. When the circuit schematics are processed, they are checked for errors. These errors are both errors like component not found, unmatching jump labels etc. Also, connectivity errors are found - broken branches, jumps next to nodes etc. This checking is done almost every time because the simulator uses the "component_objects" dictionary of Python objects to contain information about every component found in the circuit. This is essential because different types of components have different classes - example resistors have Resistor class while voltage sources have VoltageSource class. Every time a class is found, it is instantiated and added to component_objects. So component_objects is the key to processing circuit components. By having the process_circuit_schematics() method in the SimulationData class, it is available to all other classes that inherit it.

Another non-obvious class method in update_db(). The database contains every circuit component in its records. And the details include the cell position of the circuit component and the polarity if any. These can always change as a user can move the components around in the schematic spreadsheet. The update_db() method updates the information in the schematic with the database.

The SimulationData class is the base class as the above methods are needed for every simulation. The next layer in the class hierarchy is the control class which is called ListControlVariables class. This is because there may be some simulations that don't have control. However, to figure out how a control function works, the circuit files will need to be processed and this means control needs SimulationData. ListControlVariables is as below:

class ListControlVariables(SimulationData):
"""
This super class extracts all the control files and
the special variables and their parameters.
"""
def get_control_file(self):
if 'control_id' in self.kwargs:
self.control_id = int(self.kwargs['control_id'])
self.config_control_file = models.ControlFile.objects.get(\
id=self.control_id)
return
def get_control_variables(self, *args, **kwargs):
self.get_sim_model()
self.get_control_file()
try:
control_input_list = \
self.config_control_file.controlinputs_set.all()
except:
control_input_list = []
if control_input_list:
input_component_list = [input_item for input_item in control_input_list]
else:
input_component_list = []
try:
control_output_list = \
self.config_control_file.controloutputs_set.all()
except:
control_output_list = []
if control_output_list:
output_component_list = [output_item for output_item in control_output_list]
else:
output_component_list = []
try:
control_staticvar_list = \
self.config_control_file.controlstaticvariable_set.all()
except:
control_staticvar_list = []
if control_staticvar_list:
staticvar_component_list = [staticvar_item for staticvar_item in control_staticvar_list]
else:
staticvar_component_list = []
try:
control_timeevent_list = \
self.config_control_file.controltimeevent_set.all()
except:
control_timeevent_list = []
if control_timeevent_list:
timeevent_component_list = [timeevent_item for timeevent_item in control_timeevent_list]
else:
timeevent_component_list = []
try:
control_varstore_list = \
self.sim_para_model.controlvariablestorage_set.all().\
filter(control_file_name=self.config_control_file.control_file_name)
except:
control_varstore_list = []
if control_varstore_list:
varstore_component_list = [varstore_item for varstore_item in control_varstore_list]
else:
varstore_component_list = []
return [
input_component_list,
output_component_list,
staticvar_component_list,
timeevent_component_list,
varstore_component_list,
]
def get_control_context(self, *args, **kwargs):
control_component_list = self.get_control_variables(*args, **kwargs)
control_context = {}
control_context['sim_id'] = self.sim_id
control_context['control_id'] = self.control_id
control_context['control_component_list'] = control_component_list
return control_context

This class provides method to perform repeated tasks with respect to control files. Extracting the control file model instance. Providing a list of all special variables in the control file as context variables.

With these two classes implemented the amount of times the process circuit schematic code was written to get back the dictionary of component_objects or that of components_found has decreased. Moreover, with component_objects and component_founds class attributes rather than regular variables, the function call is much less cumbersome as compared to before.

The next blog post will describe how class based views for listing circuit items will be created.