6
\$\begingroup\$

I'm new to programming, especially Python (started in python3) and I want to ask if this piece of code is good enough or not. This is part of my personal project, that is, to make a personal library on chemistry. Also, if this is allowed, I want to ask if, for the following code, lookup is better than calculation.


This is a function that can give either the full or compact electron configuration given the number of electrons (electrons). In this function, the full configuration depends on the compact configuration.

To get the compact configuration, the greatest number of electrons in the inert elements less than the given number of electrons is found (inert_number). The 'position' of the writer with respect to the subshells is determined based on inert_number (subshell_index). From subshell_index, the number of electrons per subshells is given (base). Subtracting inert_number from electrons, we have copy. A loop will start which will end if base >= copy. The result will be a list starting with the atomic symbol of the element in square brackets (inert_head), followed by the remaining electrons in subshells. The exceptions to this are the elements with unusual configurations where their configurations are found through a lookup and the first two elements whose compact configurations do not start with an atomic symbol.

To get the full configuration, the compact configuration will be taken first. While the first element of the configuration is an atomic symbol, the process will be recursive. The first element will be taken, making a lookup to get the atomic number and the compact configuration will be found. The results of this will be appended to get the full configuration.

inerts_number = [2, 10, 18, 36, 54, 86, 118]
inerts_config_index = [1, 3, 5, 8, 11, 15, 19]

atomic_symbols =  [
    "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", 
    "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", 
    "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", 
    "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", 
    "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", 
    "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", 
    "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", 
    "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", 
    "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", 
    "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", 
    "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", 
    "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og"]

elems_with_odd_configs = [
    24, 29, 41, 42, 44, 
    45, 46, 47, 57, 58, 
    64, 78, 79, 89, 90, 
    91, 92, 93, 96]

configs_of_odds = [
    ["[Ar]", 1, 5],
    ["[Ar]", 1, 10],
    ["[Kr]", 1, 4], 
    ["[Kr]", 1, 5], 
    ["[Kr]", 1, 7],
    ["[Kr]", 1, 8],
    ["[Kr]", 0, 10],
    ["[Kr]", 1, 10],
    ["[Xe]", 2, 0, 1],
    ["[Xe]", 2, 1, 1],
    ["[Xe]", 2, 7, 1],
    ["[Xe]", 1, 14, 9],
    ["[Xe]", 1, 14, 10],
    ["[Rn]", 2, 0, 1],
    ["[Rn]", 2, 0, 2],
    ["[Rn]", 2, 2, 1],
    ["[Rn]", 2, 3, 1],
    ["[Rn]", 2, 4, 1],
    ["[Rn]", 2, 7, 1]]

subshell_value = [
    2, 2, 6, 2, 6, 
    2, 10, 6, 2, 10, 
    6, 2, 14, 10, 6, 
    2, 14, 10, 6]

def electron_config(electrons: int, config_type: str = 'compact') -> list:
    def __compact_config(electrons: int) -> list:
        electron_config = []
        inert_number = 0
        subshell_index = 0

        if electrons > 2:
            inert_number = max([x for x in inerts_number if x < electrons])
            subshell_index = inerts_config_index[inerts_number.index(inert_number)]
            inert_head = f'[{atomic_symbols[inert_number - 1]}]'
            electron_config.append(inert_head)
        else:
            inert_number = 0

        if electrons in elems_with_odd_configs:
            return configs_of_odds[elems_with_odd_configs.index(electrons)]
        else:
            copy = electrons - inert_number
            if electrons > 2:
                index = subshell_index
            else:
                copy = electrons
                index = 0
            while True:
                base = subshell_value[index]
                if base < copy:
                    electron_config.append(base)
                    copy -= base
                    index += 1
                else:
                    if copy > 0:
                        electron_config.append(copy)
                    break
            return electron_config
    
    def __full_config(electrons: int) -> list:
        temp_config = []
        compact_config = __compact_config(electrons)
        inert_elem = ''

        if electrons > 2:
            inert_elem = str(compact_config[0]).removeprefix('[').removesuffix(']')
            inert_electrons = atomic_symbols.index(inert_elem) + 1
            if inert_elem in ['He', 'Ne', 'Ar', 'Kr', 'Xe', 'Rn']:
                temp_config = __full_config(inert_electrons) + compact_config[1:]
        else:
            temp_config = __compact_config(electrons)

        return temp_config

    if config_type == 'compact':
        return __compact_config(electrons)
    elif config_type == 'full':
        return __full_config(electrons)
    else:
        raise ValueError(f'Expected \'compact\' or \'full\', received {config_type} instead')

An alternative way I did was to add inert_number and subshell_index as optional parameters to __compact_config with None as default values. If both are None, proceed as usual. Then, to get the full configuration, __compact_config will be called and inert_number = 0, subshell_index = 0. This will be the changes:

    ...
    def __compact_config(electrons: int, inert_number: int = None, subshell_index: int = None) -> list:
        ...
        if inert_number is None and subshell_index is None:
            if electrons > 2:
                inert_number = max([x for x in inerts_number if x < electrons])
                subshell_index = inerts_config_index[inerts_number.index(inert_number)]
                inert_head = f'[{atomic_symbols[inert_number - 1]}]'
                electron_config.append(inert_head)
            else:
                inert_number = 0
        ...
    def __full_config(electrons: int) -> list:
        return __compact_config(electrons, inert_number = 0, subshell_index = 0)

In __full_config, is it better to use the recursive way to show what the 'usual' way is when finding the electron configuration, or start from the first subshell using the writer from __compact_config?

\$\endgroup\$

2 Answers 2

7
\$\begingroup\$

Your database literals (atomic_symbols, etc.) should all be tuples () instead of lists [] for immutability. However, as I show below, some are better-suited to dictionaries instead of sequences.

Your electron_config function is really just two functions: one for compact, and one for full. If you were to keep them as one function, config_type should not be hinted as str, but as Literal['compact', 'full']. But the two halves share no logic. Better to extract your two inner functions as outer functions. Also, those inner functions - since they were in function scope - are never visible to the outside, currently, so there's no point in prefixing them with underscores.

inert_elem in ['He', 'Ne', 'Ar', 'Kr', 'Xe', 'Rn'] should use a set literal {}, and that literal should be moved to the same location as your other database constants.

This:

.removeprefix('[').removesuffix(']')

is evidence that you have premature formatting on your element name. I don't see why you would surround the element name in brackets at all, but if you must, it should be done later in a formatting function.

Your return format is strange: it's "maybe" a string, followed by a list of integers. Best to separate this into a

tuple[
    Optional[str],
    list[int]
]

or, better, replace the list with a generator and don't successive-append().

Add unit tests. I've only shown regression tests.

Some of your data structure patterns are non-ideal. For instance, elems_with_odd_configs should really just be the keys of configs_of_odds refactored as a dictionary. Your max search is also not ideal, and should be replaced with a call to bisect_left to find the closest appropriate value in logarithmic instead of linear time (though the performance will not be noticeable given how small the sequence is). Avoid .index() when possible.

Rather than maintaining your own index into SUBSHELL_VALUE, just iterate base over a slice.

A light refactor covering some of the above is

from typing import Optional, Iterable, Iterator
from bisect import bisect_left

INERTS_CONFIG_INDEX = (1, 3, 5, 8, 11, 15, 19)
INERTS_NUMBER = (2, 10, 18, 36, 54, 86, 118)

ATOMIC_SYMBOLS = (
    'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne',
    'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca',
    'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn',
    'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y', 'Zr',
    'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn',
    'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd',
    'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb',
    'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg',
    'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac', 'Th',
    'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm',
    'Md', 'No', 'Lr', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds',
    'Rg', 'Cn', 'Nh', 'Fl', 'Mc', 'Lv', 'Ts', 'Og',
)

CONFIGS_OF_ODDS = {
    24: ('Ar', (1, 5)),
    29: ('Ar', (1, 10)),
    41: ('Kr', (1, 4)),
    42: ('Kr', (1, 5)),
    44: ('Kr', (1, 7)),
    45: ('Kr', (1, 8)),
    46: ('Kr', (0, 10)),
    47: ('Kr', (1, 10)),
    57: ('Xe', (2, 0, 1)),
    58: ('Xe', (2, 1, 1)),
    64: ('Xe', (2, 7, 1)),
    78: ('Xe', (1, 14, 9)),
    79: ('Xe', (1, 14, 10)),
    89: ('Rn', (2, 0, 1)),
    90: ('Rn', (2, 0, 2)),
    91: ('Rn', (2, 2, 1)),
    92: ('Rn', (2, 3, 1)),
    93: ('Rn', (2, 4, 1)),
    96: ('Rn', (2, 7, 1)),
}

SUBSHELL_VALUE = (
    2, 2, 6, 2, 6,
    2, 10, 6, 2, 10,
    6, 2, 14, 10, 6,
    2, 14, 10, 6,
)

INERT_ELEMS = {
    'He', 'Ne', 'Ar', 'Kr', 'Xe', 'Rn',
}


def generate_electrons(index: int, copy: int) -> Iterator[int]:
    for base in SUBSHELL_VALUE[index:]:
        if base >= copy:
            if copy > 0:
                yield copy
            break
        yield base
        copy -= base


def make_compact_config(electrons: int) -> tuple[
    Optional[str],  # inert head
    Iterable[int],  # electron counts
]:
    odd_config = CONFIGS_OF_ODDS.get(electrons)
    if odd_config is not None:
        return odd_config

    if electrons <= 2:
        return None, generate_electrons(index=0, copy=electrons)

    inert_index = bisect_left(INERTS_NUMBER, electrons) - 1
    inert_number = INERTS_NUMBER[inert_index]
    subshell_index = INERTS_CONFIG_INDEX[inert_index]
    inert_head = ATOMIC_SYMBOLS[inert_number - 1]
    return inert_head, generate_electrons(index=subshell_index, copy=electrons - inert_number)


def make_full_config(electrons: int) -> Iterator[int]:
    inert_elem, compact_config = make_compact_config(electrons)

    if electrons > 2:
        inert_electrons = ATOMIC_SYMBOLS.index(inert_elem) + 1
        if inert_elem in INERT_ELEMS:
            yield from make_full_config(inert_electrons)

    yield from compact_config


def test() -> None:
    inert_head, config = make_compact_config(1)
    assert inert_head is None
    assert tuple(config) == (1,)

    inert_head, config = make_compact_config(10)
    assert inert_head == 'He'
    assert tuple(config) == (2, 6)

    inert_head, config = make_compact_config(42)
    assert inert_head == 'Kr'
    assert tuple(config) == (1, 5)

    inert_head, config = make_compact_config(100)
    assert inert_head == 'Rn'
    assert tuple(config) == (2, 12)

    assert tuple(make_full_config(1)) == (1,)

    assert tuple(make_full_config(10)) == (2, 2, 6)

    assert tuple(make_full_config(42)) == (2, 2, 6, 2, 6, 2, 10, 6, 1, 5)

    assert tuple(make_full_config(100)) == (2, 2, 6, 2, 6, 2, 10, 6, 2, 10, 6, 2, 14, 10, 6, 2, 12)


if __name__ == '__main__':
    test()
\$\endgroup\$
3
  • \$\begingroup\$ I provided two possible ways for __full_config and I want to know which one is better. Can I ask for a follow-up comment? \$\endgroup\$
    – soupless
    Commented Mar 11, 2022 at 13:56
  • \$\begingroup\$ Kind of neither? See edit \$\endgroup\$
    – Reinderien
    Commented Mar 11, 2022 at 16:54
  • 1
    \$\begingroup\$ The keyword yield is new to me. I'll learn your code and apply your reviews. I also need to learn the data types to properly assign the type of the return types. Thank you. \$\endgroup\$
    – soupless
    Commented Mar 11, 2022 at 17:36
4
\$\begingroup\$

Don't commingle core algorithm and presentation concerns. Your algorithm builds strings like [He] to hold the inert head. But then in other parts of your code you have to reverse such string manipulations to get just the He. Instead of doing that, organize your data structures and values around the fundamental values or importance. Later, for presentation purposes, you might want to glue the square brackets to the inert head, but that will be easy to do late in the game rather than in the middle of your fundamental calculations.

Name constants appropriately. You program has several important constants, but they are named like variables.

Organize data structures to facilitate the algorithm, part 1. Your current data structures are quite awkward. You have a list of atomic_symbols, but you never use the list directly. What you really need is a lookup from electrons to symbols.

SYMS =  (
    'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne',
    'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca',
    'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn',
    'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y', 'Zr',
    'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn',
    'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd',
    'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb',
    'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg',
    'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac', 'Th',
    'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm',
    'Md', 'No', 'Lr', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds',
    'Rg', 'Cn', 'Nh', 'Fl', 'Mc', 'Lv', 'Ts', 'Og',
)

SYMBOLS = dict(zip(range(1, len(SYMS) + 1), SYMS))

Organize data structures to facilitate the algorithm, part 2. Some of your other constants cause trouble because they require awkward index lookups and reference to parallel data structures. Here are two examples:

# Example 1.
subshell_index = inerts_config_index[inerts_number.index(inert_number)]

# Example 2.
return configs_of_odds[elems_with_odd_configs.index(electrons)]

Something much simpler will work fine with just a few adjustments in the code:

# Just the numbers are sufficient.
# You don't need inerts_config_index.
INERT_NUMBERS = (2, 10, 18, 36, 54, 86, 118)

# I don't know your domain well, so adjust naming as needed.
SUBSHELL_VALUES = (
    2,
    2, 6,
    2, 6,
    2, 10, 6,
    2, 10, 6,
    2, 14, 10, 6,
    2, 14, 10, 6,
)

# Create a dict so you can directly access the special compact-configs
# rather than jumping through hoops via index lookups.
SPECIAL_COMPACTS = {
    24: (1, 5),
    29: (1, 10),
    41: (1, 4),
    42: (1, 5),
    44: (1, 7),
    45: (1, 8),
    46: (0, 10),
    47: (1, 10),
    57: (2, 0, 1),
    58: (2, 1, 1),
    64: (2, 7, 1),
    78: (1, 14, 9),
    79: (1, 14, 10),
    89: (2, 0, 1),
    90: (2, 0, 2),
    91: (2, 2, 1),
    92: (2, 3, 1),
    93: (2, 4, 1),
    96: (2, 7, 1),
}

Create a meaningful object to store an electron configuration. Currently, you code requires the user to ask for either a full config or compact config. But the two are closely related (one is a kind of subset of the other), so it seems more straightforward to return some kind of data structure or object that would hold both configs at once. In addition, your current compact-configs are awkward because they put disparate data in a single collection (a string plus some numbers). Finally, your electron configs lack some useful ancillary information: facts like the number of electrons and the atomic symbol. In the code below, I use a class to bundle everything together. If you want to avoid OO for now, you could instead return something like a dict. Either way, the point is to create a meaningful, declarative bundle of information.

Your algorithm to compute the configs is more complex than needed. I know you are particularly interested in feedback on the alternative ways in your current code to create the full and compact configs. However, I think your current code is far too complex. Instead, I would encourage you to consider a different approach -- one that requires no recursion at all. The logic goes through three phases: (1) a naive full-config based purely on SUBSHELL_VALUES; (2) a naive compact-config that pops values off of that naive full-config; and (3) a resolution phase where we use a special compact-config, if any, and adjust the full-config accordingly.

class ElectronConfig:

    def __init__(self, electrons):
        # Symbol and N of electrons.
        self.symbol = SYMBOLS[electrons]
        self.electrons = electrons

        # Inert head.
        inerts = [n for n in INERT_NUMBERS if n < electrons]
        inert_number = max(inerts, default = 0)
        self.inert_head = SYMBOLS.get(inert_number, None)

        # A naive full-config based only on SUBSHELL_VALUES.
        full = []
        tot = 0
        for ssv in SUBSHELL_VALUES:
            tot += ssv
            full.append(ssv)
            diff = tot - electrons
            if diff >= 0:
                full[-1] -= diff
                break

        # A naive compact-config, created by removing values
        # from the end of the naive full-config.
        compact = []
        tot = 0
        while tot < electrons - inert_number:
            ssv = full.pop()
            tot += ssv
            compact.append(ssv)
        compact.reverse()

        # Set the actual configs.
        self.compact = SPECIAL_COMPACTS.get(electrons, tuple(compact))
        self.full = tuple(full) + self.compact
\$\endgroup\$
2
  • \$\begingroup\$ I presented an alternative approach to the full configuration, but Reinderien said that neither of them are good. \$\endgroup\$
    – soupless
    Commented Mar 12, 2022 at 6:16
  • \$\begingroup\$ @soupless Neither is ideal, but neither is terrible either. I didn't really form an opinion as to which was better, and ended up presenting a modified version of your recursive solution. \$\endgroup\$
    – Reinderien
    Commented Mar 13, 2022 at 13:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.