I faced the same problem in my time, so I ended up writing a function like this.

```
def select_index(used_indices):
indeces = r'w v u s r q o n m l k j i h g f e d c b a'.split()
for uind in indeces:
found = False
for qind in used_indices:
if qind == uind:
found = True
break
if not found:
used_indices.add(uind)
index = uind
break
return Ex(index), used_indices
def one_nabla(ex, used_indices):
t3, used_indices = select_index(used_indices)
free = dict()
free['sub'] = set()
free['up'] = set()
for nabla in ex[r'\nabla']:
nabla.name=r'\partial'
dindex = nabla.indices().__next__()
for arg in nabla.args():
ret:=0;
for index in arg.free_indices():
if index.parent_rel==sub:
free['sub'].add(str(index.ex()))
else:
free['up'].add(str(index.ex()))
for key in free.keys():
for index in free[key]:
ind = Ex(index)
if key == 'sub':
t1:= -\Gamma^{@[t3]}_{@(dindex) @[ind]};
else:
t1:= \Gamma^{@[ind]}_{@(dindex) @[t3]};
t2:= @[arg];
for term_index in arg.free_indices():
if str(term_index.ex()) == index:
if term_index.parent_rel==sub:
t2[term_index]:= _{@[t3]};
else:
t2[term_index]:= ^{@[t3]};
ret += Ex(str(nabla.multiplier)) * t1 * t2
nabla += ret #
break
break
return ex, used_indices
def nabla_calculation(ex, used_indices, count):
ex = ex.ex()
for element in ex.top().terms():
new:=1;
local_count = 0
for nabla in element[r'\nabla']:
local_count += 1
if local_count == 0:
continue
elif local_count == 1:
new, used_indices = (one_nabla(element, used_indices))
element.replace(new.ex())
count -= 1
else:
if element.ex().top().name == r'\prod':
i = 0
while i < 2:
for mult in element.ex().top().children():
local_count2 = 0
for nabla in mult[r'\nabla']:
local_count2 += 1
if local_count2 == 0:
new *= mult.ex()
elif local_count2 == 1:
for nabla in mult[r'\nabla']:
nabla1, used_indices = one_nabla(nabla, used_indices)
nabla.replace(nabla1.ex())
new *= nabla.ex()
else:
mult1, used_indices = nabla_calculation(mult, used_indices, local_count)
new *= mult1
i+=1
new *= Ex(str(element.multiplier))
element.replace(new)
else:
for nabla in element[r'\nabla']:
for arg1 in nabla.args():
arg2, used_indices = nabla_calculation(arg1, used_indices, count - 1)
index = nabla.indices().__next__()
t := \nabla_{@(index)}{@[arg2]};
new = t
nabla1, used_indices = one_nabla(new, used_indices)
new = Ex(str(nabla.multiplier)) * nabla1
nabla.replace(new)
return ex, used_indices
def expand_nabla(ex):
if ex.top().name == '\equals':
for child in ex.top().children():
ret:=0;
for element in child.ex().top().terms():
count = 0
used_indices = set()
for nabla in element.ex()[r'\nabla']:
count += 1
if count == 0:
ret += element.ex()
#new_ex += element.ex()
else:
for n in element.ex():
for index in n.indices():
used_indices.add(str(index))
#for nabla in element[r'\nabla']:
element1, used_indices = nabla_calculation(element, used_indices, count)
ret +=element1
child.replace(ret)
else:
for element in ex.top().terms():
count = 0
used_indices = set()
for nabla in element.ex()[r'\nabla']:
count += 1
if count == 0:
pass
else:
for n in element.ex():
for index in n.indices():
used_indices.add(str(index))
element1, used_indices = nabla_calculation(element, used_indices, count)
element.replace(element1)
return ex
```

To use it, you need to call the main function expand_nabla(ex). It is suitable for large expressions, does not duplicate already occupied indexes, and also knows how to work with equalities.