Repository: dylanwal/BigSMILES (language: python)

Issue:
Docs
adding docs


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
new file mode 100644
index 0000000..f8e57ec
--- /dev/null
+++ b/.github/workflows/build-docs.yml
@@ -0,0 +1,15 @@
+name: Build Docs
+on:
+  push:
+    branches:
+      - master
+jobs:
+  build-docs:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: 3.x
+      - run: pip install -r requirements_dev.txt
+      - run: mkdocs gh-deploy --clean --force
diff --git a/README.md b/README.md
index 761def6..df0a40b 100644
--- a/README.md
+++ b/README.md
@@ -3,17 +3,19 @@
 ---
 ---
 
+![PyPI](https://img.shields.io/pypi/v/bigsmiles)
+![downloads](https://static.pepy.tech/badge/bigsmiles)
+![license](https://img.shields.io/github/license/dylanwal/bigsmiles)
 
 **(still under development; but usable)**
 
-SMILES (simplified molecular-input line-entry system) representation is a line notation for molecules with 
-given deterministic molecular structures. **BigSMILES** is an extension to SMILES which provides support for molecules 
-that contain stochastic molecular structures. The code here parses the string into and abstract syntax tree.
-
-[Learn more about BigSMILES Notation](https://olsenlabmit.github.io/BigSMILES/docs/line_notation.html#the-bigsmiles-line-notation)
-
+**SMILES** (simplified molecular-input line-entry system) representation is a line notation for molecules with 
+given deterministic molecular structures. 
 
+**BigSMILES** is an extension to SMILES which provides support for molecules 
+that contain stochastic molecular structures. The code here parses the string into and abstract syntax tree.
 
+[**Documention**](https://dylanwal.github.io/BigSMILES/)
 
 
 ---
@@ -30,182 +32,32 @@ Pip installable package available
 ---
 
 ## Requirements / Dependencies
-Python 3.10+
+Python 3.7+
 
 
 ---
 
 ## Basic Usage
 
-### Print Tree
 #### Code:
 ```python
-import bigsmiles
+import bigsmiles as bs
 
 polymer_string = "CC{[>][<]CC(C)[>][<]}CC(C)=C"
-polymer = bigsmiles.BigSMILES(polymer_string)
-polymer.print_tree()
+polymer = bs.BigSMILES(polymer_string)
 ```
 
-#### Output:
-```python
-BigSMILES: CC{[>][<]CC(C)[>][<]}CC(C)=C
-├── Atom: C
-├── Bond: 
-├── Atom: C
-├── Bond: 
-├── StochasticObject: {[>][<]CC(C)[>][<]}
-│    └── StochasticFragment: [<]CC(C)[>]
-│        ├── BondDescriptorAtom: [<]
-│        ├── Bond: 
-│        ├── Atom: C
-│        ├── Bond: 
-│        ├── Atom: C
-│        ├── Branch: (C)
-│        │    ├── Bond: 
-│        │    └── Atom: C
-│        ├── Bond: 
-│        └── BondDescriptorAtom: [>]
-├── Bond: 
-├── Atom: C
-├── Bond: 
-├── Atom: C
-├── Branch: (C)
-│    ├── Bond: 
-│    └── Atom: C
-├── Bond: =
-└── Atom: C
-```
 
----
----
+## Documentation
 
-## Abstract Syntax Tree
-
-root node: `BigSMILES` 
-
-intermediate nodes: `StochasticObject`, `StochasticFragment`, `Branch`
-
-leaf nodes: `BondDescriptorAtom`, `Atom`, `Bond`
-
-The tree structure is built through the `nodes` attribute.
-
-**Note:** only main attributes shown in diagram below.
-
-```mermaid
-classDiagram
-
-    class BigSMILES {
-        list: nodes
-    }
-    
-    
-    class StochasticObject {
-        int: id_
-        list: nodes
-        BondingDescriptor: end_group_left
-        BondingDescriptor: end_group_right
-    }
-    
-    
-    class StochasticFragment {
-        int: id_
-        list: nodes
-    }
-    
-    
-    class Branch {
-        int: id_
-        list: nodes
-    }
-    
-    
-    class BondDescriptorAtom {
-        int: id_
-        BondDescriptor: descriptor
-        Bond: bond
-    }
-    
-    
-    class BondDescriptor {
-        str: symbol
-        int: index_
-        Enum: type_
-        list[BondDescriptorAtom]: instances
-    }
-
-    
-    class Bond {
-        int: id_
-        str: symbol
-        Enum: type_
-        Atom: atom1
-        Atom: atom2
-        int: ring_id
-    }
-    
-    class Atom {
-        int: id_
-        str: symbol
-        Enum: type_
-        int: isotope
-        int: charge
-        Enum: chiral
-        int: valance
-        bool: orgainic
-        list[Bond]: bonds
-    }
-
-    BigSMILES --|> Atom
-    BigSMILES --|> Bond
-    BigSMILES --|> Branch
-    BigSMILES --|> StochasticObject
-    StochasticObject --|> StochasticFragment
-    StochasticFragment --|> BondDescriptorAtom
-    BondDescriptor --|> BondDescriptorAtom
-    StochasticFragment --|> Atom
-    StochasticFragment --|> Bond
-    StochasticFragment --|> Branch
-    StochasticFragment --|> StochasticObject
-    Branch --|> BondDescriptorAtom
-    Branch --|> StochasticObject
-    Branch --|> Bond
-    Branch --|> Atom
-    
-```
+For quickstart, tutorials, reference material, BigSMILES, everything...  see documentation: 
 
----
----
-
-## Advanced Options
-
-### Colored outputs
-```python
-import bigsmiles
-
-bigsmiles.Config.color_output = True
-```
-
-BigSMILES: (html and letex verions shown)
-
-CC
-<span style="color:red">{</span>
-<span style="color:green">[>]</span>
-<span style="color:green">[<]</span>
-CC(C)
-<span style="color:green">[>]</span>
-<span style="color:green">[<]</span>
-<span style="color:red">}</span>
-CC(C)=C
+[**Documention**](https://dylanwal.github.io/BigSMILES/)
 
 
 ## Features NOT implemented yet
-* Cis/Trans
-* Fragment Notation
 * ladder polymers
-* mixture notation '.'
-* reactions
-* Validation
-  * Some is present; but more needed:
-    * Validate bonding descriptors matching including endgroups
-
+* Validation is not comprehensive
+* Not all aromatic rings are processed correctly at the moment 
+... Lots more to come
+* 
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..be6c1aa
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,14 @@
+# Instruction for creating docs
+
+
+
+## To view docs locally
+Enter this into your terminal: `mkdocs serve`
+
+
+
+# css colors
+https://github.com/squidfunk/mkdocs-material/blob/master/src/assets/stylesheets/main/_colors.scss 
+
+# mkdocs.yml
+https://mkdocstrings.github.io/python/usage/
diff --git a/docs/bigsmiles/examples.md b/docs/bigsmiles/examples.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/bigsmiles/old.txt b/docs/bigsmiles/old.txt
new file mode 100644
index 0000000..3bf6238
--- /dev/null
+++ b/docs/bigsmiles/old.txt
@@ -0,0 +1,869 @@
+---
+title: BigSMILES Line Notation
+
+language_tabs: # must be one of https://git.io/vQNgJ
+  - plaintext: BigSMILES Examples
+
+toc_footers:
+  - <a href='index.html'>BigSMILES Project</a>
+  - <a href='line_notation.html'>BigSMILES Line Notation</a>
+  - <a href='DataFormat.html'>BigSMILES Data Format</a>
+  - <a href='https://github.com/olsenlabmit/bigSMILES'>GitHub</a>
+  - <a href='https://github.com/lord/slate'>Documentation Powered by Slate</a>
+
+includes:
+
+search: true
+---
+
+# The BigSMILES Line Notation
+
+> The BigSMILES string for ethylene and methyl methacrylate copolymer:
+>
+> <img src="images/Demo1.png" alt="alt text" width="50%;" />
+
+BigSMILES is a structurally based line notation based on the highly successful SMILES (simplified molecular-input line-entry system) representation.  It provides extensions over regular SMILES, which can only represent molecules with given deterministic molecular structures, and support the representation of molecules that are characterized by a distribution of distinct molecular structures instead of a single well-defined structure. This feature is designed specifically to reflect the stochastic nature of polymer molecules. Since polymers are macromolecules that usually have large molecular weights, the line notation is termed BigSMILES.
+
+
+
+<aside class="notice">
+Please note that the BigSMILES line notation continually revised to provide additional features and important bug fixes. This page hosts the latest version of BigSMILES, which may differ from the syntax discussed in the <a href="https://pubs.acs.org/doi/10.1021/acscentsci.9b00476">original paper</a>. <br/>
+The current version is v1.1. Please check the <a href="#version-history">version history</a> for the detailed log of changes.
+</aside>
+
+
+
+This article is meant as an documentation for the BigSMILES syntax. The reader is assumed to be familiar with the SMILES syntax. If you are not familiar with the syntax of SMILES, this Daylight CIS [page](https://daylight.com/dayhtml/doc/theory/theory.smiles.html) provides a great introduction to the subject.
+
+
+
+For additional examples, this [page](BigSMILES_examples.md#Examples-of-BigSMILES-Encoding) provides examples of how BigSMILES can be used to encode common commodity polymers and polymers selected from literature with complex structures.
+
+
+
+# Syntax
+
+The syntax of BigSMILES is identical to SMILES except for the introduction of the following new objects:
+
+1. the stochastic object, and
+2. the bonding descriptors
+
+The ***stochastic object*** is the principle extension that enables the enumeration of stochastic molecular structures. A stochastic object represents a family of polymeric fragments. It could be used as if it was a normal atom and be embedded within SMILES strings as a proxy atom that symbolizes the ensemble of polymeric fragments that it implicitly represents. 
+
+<img src="images/StoObj1.png" alt="alt text" width="100%;" />
+
+The family of structures a stochastic object represent is defined by the building blocks, or repeat units, of the polymeric segment. Within a stochastic object, one or more repeat units are tabulated, and the pattern by which the repeat units can interconnect with each other are specified. In BigSMILES, such connectivity patterns are denoted by attaching ***bonding descriptors*** to atoms to indicate where and how a pair of atoms on two repeat units can be joined into a larger molecular fragment. The syntax for the bonding descriptors and stochastic objects will be introduced in subsequent sections.
+
+## Bonding Descriptors
+
+To specify where and how repeat units can be joined with another repeat unit, the bonding descriptors are introduced. Bonding descriptors are placed on atoms of a repeat unit that could form direct bonds with another repeat unit.
+
+<img src="images/BondDesc1.png" alt="alt text" width="80%;" />
+
+For instance, as illustrated by the figure, on a methyl methacrylate repeat unit, the two backbone carbon atoms that connect with other repeat units each has a bonding descriptor attached to it; similarly, for a glycine repeat unit, the nitrogen and carbon atoms on the two termini also have bonding descriptors directly attached, indicating that they are terminal atoms that connect to other repeat units.
+
+In BigSMILES, there are two types of bonding descriptors:
+
+1. The "`$`" descriptor, or AA-type descriptor    
+   This descriptor describes the connective pattern in which any atom with a "`$`" descriptor can connect with any other atom with the same bonding descriptor. 
+2. The "`<`" and "`>`" descriptors, or AB-type descriptor  
+   This pair of descriptors describes the connective pattern in which the atoms with the "`<`" descriptor could only connect with another atom with the conjugate "`>`" descriptor. 
+
+### AA-Type Descriptor
+
+> The bonding descriptors can be written as if they are atoms. For example, the ethylene repeat unit for the poly(ethylene-butene) example reads
+
+```plaintext
+[$]-CC-[$] or [$]CC[$] 
+```
+
+> For the butene repeat unit, its BigSMILES read
+
+```plaintext
+[$]CC(CC)[$]
+```
+
+> In general, the repeat unit can be rewritten without the bonding descriptors being at the ends of the string. For instance, the following BigSMILES string
+
+```plaintext
+C([$])C([$])CC
+```
+
+> represents the exact same butene repeat unit.
+
+The AA-type of descriptor is useful for describing the connectivity pattern for vinyl polymers, in which the backbone carbon atoms can connect any other vinyl carbon (although some configurations are unlikely). 
+<img src="images/descriptorDemo1.png" alt="alt text" style="zoom:40%;" />
+For instance, the use of the AA-type descriptor can be illustrated by the example of poly(ethylene-butene). As illustrated in the figure, the polymer is represented by two repeat units, the ethylene and butene units. Since any pair of atoms with a `$` descriptor could be joined to form a dimer, for this polymer, there are 6 possible distinct dimers that could be constructed from the repeat units, as tabulated on the right panel. These dimers can be further joined with other repeat units or higher order oligomers to generate other polymeric fragments.
+
+### AB-Type Descriptor
+
+> AB-type descriptor has syntax similar to the AA-type descriptor. For example, the repeat units for the first nylon-6,6 are
+
+```plaintext
+[<]C(=O)CCCCC(=O)[<] and [>]NCCCCCCN[>]
+```
+
+> whereas the second nylon example reads
+
+```plaintext
+[<]C(=O)CCCCC(=O)NCCCCCCN[>]
+```
+
+> Another instance where the AB-type bonding descriptors become useful is polymers synthesized through ring-opening polymerization. For example, poly(ethylene glycol) repeat unit would read
+
+```plaintext
+[<]CCO[>] or [>]CCO[<]
+```
+
+> <img src="images/PEG.png" alt="alt text" width="25%;" />
+
+
+
+The AB-type descriptors are useful in describing polymers formed via polycondensation. 
+<img src="images/descriptorDemo2.png" alt="alt text" style="zoom:40%;" />
+
+For instance, nylon-6,6 formed from condensation between amine and carboxylic groups are best described by the `<` and `>` descriptors, which ensure that the nitrogen atoms always connect with the carbon atoms, in accordance with the chemistry.
+
+<aside class="notice">
+It should be noted that the connectivity pattern specified by bonding descriptors include all "possible" realizations. The descriptors do not indicate whether such connection is likely or not. As detailed in the next section, the descriptors, together with the stochastic object, describes an "unweighted" ensemble of all possible molecular fragments.
+</aside>
+
+### Syntax for Bonding Descriptors
+
+> Bonding descriptors can be embedded within BigSMILES strings just like atoms. For instance, a (hypothetical) ethylene unit with two orthogonal bonding descriptors reads
+
+```plaintext
+[$1]-CC-[$2] or [$1]CC[$2] or C([$1])C[$2]
+```
+
+> A repeat unit can have more than two atoms that are connected to a bonding descriptor. For instance, consider branched poly(ethylene). A branched PE would be consisted of repeat units with 2 bonding descriptors (linear), 3 bonding descriptor (branching site) or even more descriptors.
+
+```plaintext
+[$]-CC-[$] (linear unit),  [$]-CC([$])-[$] (branched unit)
+```
+
+> In the previous examples, the SMILES bonds leading up to the bonding descriptors were all single bonds. This implies that when two repeat units are joined into a dimer, the bond between the pair of atoms would be a single bond as well. 
+>
+> To represent polymers with double bond across repeat units, a similar syntax is used
+
+```plaintext
+[$]=CCC=[$]
+```
+
+> <img src="images/descriptorDemo3.png" alt="alt text" width="25%;" />
+
+Formally speaking, within BigSMILES strings, the bonding descriptor are denoted with the following syntax:
+
+`bonding_descriptor = '[' + descriptor_type + descriptor_id + ']'`
+
+A bonding descriptor is composed of three components:
+
+1. A bonding descriptor is enclosed by a pair of square brackets.
+2. The `descriptor_type`, immediately following the opening bracket. 
+   * The `descriptor_type` can be one of the three symbols: "`$`", "`<`" or "`>`"
+3. The `descriptor_id` of the descriptor, denoted by a number
+
+The `descriptor_id` is an additional label for specifying multiple sets of chemically orthogonal bonding descriptors. Descriptors only recognize other descriptors with identical id; atoms connected to descriptors with different labels cannot be joined. If there is only a single type of descriptor, the descriptor id may be omitted.
+
+<aside class="notice">
+The id for $ and &gt;,&lt; are implicitly considered orthogonal; i.e., $1 do not connect with either &lt;1 or &gt;1. However, while it is permissible to reuse the same id/label between the two types of descriptors, it is recommended that distinct label be used for each distinct descriptor to prevent potential confusion.
+</aside>
+
+Within a BigSMILES string, the bonding descriptors share the same syntax as normal SMILES atoms. The SMILES bond symbol between an atom and its associated bonding descriptor is interpreted as the type of bond that will be created when two atoms are joined together to form a larger fragment. Therefore, the bond order for the bond leading up to the bonding descriptor must be consistent throughout all occurrences in every repeat unit.
+
+<aside class="warning">
+The bonding descriptor syntax has been updated and is different from the <a href="https://pubs.acs.org/doi/10.1021/acscentsci.9b00476">version 1.0 presented in ACS Central Science</a>.
+</aside>
+
+## Stochastic Object
+
+SMILES provides a linearization scheme for chemical graphs. However, in general, chemical graphs with deterministic structures are not the best model for describing polymers, which are intrinsically random molecules that represent an ensemble of different chemical connectivity. To embrace such stochastic nature, BigSMILES introduces the notion of ***stochastic objects***. Stochastic objects are the key component that provides support for stochastic structures. They are entities that represent random chemical structures that corresponds to polymeric segments within molecules. Syntax-wise, a stochastic object's behavior is identical to that of SMILES atoms. Therefore, stochastic objects can be embedded within a regular SMILES string as if they are normal atoms. 
+
+<aside class="notice">
+For the current version, stochastic objects are allowed maximal valency of two. 
+</aside>
+
+### Syntax for Stochastic Objects
+
+> For the poly(ethylene-butene) example illustrated in earlier section, the stochastic object that represents the uncapped polymeric fragments that are composed of the two repeat units read
+
+```plaintext
+{[][$]CC[$],[$]CC(CC)[$][]}
+```
+
+> The "empty" terminal bonding descriptors indicate that the stochastic object does not have any connection to explicit end groups. 
+>
+> Similarly, nylon-6,6 can be represented as
+
+```plaintext
+{[][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][]}
+```
+
+> for the polymer synthesized from adipic acid and hexamethylenediamine. 
+>
+> For nylon synthesized from amino acid, the BigSMILES reads
+
+```plaintext
+{[][<]C(=O)CCCCC(=O)NCCCCCCN[>][]}
+```
+
+A stochastic object acts as an proxy within a SMILES string and represents a polymeric segment with stochastic structure. The exact ensemble of structures that a stochastic object represent is defined by is constituent **repeat units** and **end groups**. The syntax for stochastic is given by 
+
+`stochastic_object = '{' + '[' + terminal_bonding_descriptor + ']' + repeat_unit_1 + ',' + repeat_unit_2 + ',' + ... + ',' + repeat_unit_n + ';' + end_group_1 + ',' + end_group_2 + ',' + ... + ',' + end_group_m + '[' + terminal_bonding_descriptor + ']' + '}'`
+
+Each stochastic object contains the following sections:
+
+1. A pair of curly brackets "`{`" and "`}`" that encloses the inner components. The entire object wrapped within the brackets `{...}` are syntactically treated as if it is a normal atom.
+2. A comma-delimited list of `repeat_unit`s. 
+   * This list contains the set of building blocks that could be used to construct oligomeric and polymeric fragments. 
+   * Each `repeat_unit` within the list is represented by a BigSMILES string that contains two or more bonding descriptors. 
+   * The stochastic object represents the entire ensemble of polymeric fragments that could be generated by interconnecting the repeat units into larger fragments. Note that each repeat unit within the list can be used zero, one or multiple times.
+   * All possible ways of interconnecting a pair of repeat units, by means of joining two smaller molecular graphs into a larger molecular graph, that are permissible by the prescribed bonding descriptors are allowed. 
+     As illustrated by the example on the example panel, repeat units need not connect in the orientation depicted in the repeat units. Repeat units can be flipped or reoriented 
+3. A comma-delimited list of `end group`s. The end group list is separated from the repeat unit list by a semicolon. 
+   * Each end group within the list should contain exactly one bonding descriptor.
+   * If no end group is to be specified, the leading semicolon can be omitted.
+4. Two `terminal_bonding_descriptors` placed immediately within the curly brackets.   
+   * The terminal bonding descriptor indicate how the internal stochastic structure is connected to the exterior of the stochastic object (to the regular SMILES strings immediately leading and trailing the stochastic object).   
+   * Whenever an end of the stochastic object is capped with external SMILES string, the terminal bonding descriptor must be specified.  
+   * Moreover, even in the case where the end of the stochastic object is not capped by an external SMILES fragment, the "empty" bonding descriptor "`[]`" should still be explicitly included to indicate the absence of such connection.
+
+<aside class="warning">
+The terminal bonding descriptor syntax has been updated and is different from the <a href="https://pubs.acs.org/doi/10.1021/acscentsci.9b00476">version 1.0 presented in ACS Central Science</a>.
+</aside>
+
+### End Groups
+
+> Consider the ethylene butene copolymer example. For a BigSMILES string that reads
+
+```plaintext
+{[][$]CC[$],[$]CC(CC)[$][]}
+```
+
+> the "empty" terminal bonding descriptors indicate that the stochastic object does not have any connection to explicit end groups. In this case, since no implicit end groups were assigned, the BigSMILES string represents a uncapped fragment.
+>
+> Now, consider the case
+
+```plaintext
+C{[$][$]CC[$],[$]CC(CC)[$][$]}
+```
+
+> The left end in this case has explicit end group of one carbon. Moreover, since the right terminal descriptor is non-empty, it indicates that the other end of the stochastic object is also connected to some exterior SMILES string. For this case, the "invisibility" of the SMILES string suggests that it is connected to a hydrogen atom, which is omitted by default in SMILES syntax.
+>
+> To further illustrate the syntax for end groups, consider again the nylon-6,6 example. The end groups for the linear polymer can be expressed with the explicit syntax
+
+```plaintext
+[H]O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}[H]
+```
+
+> or with the implicit syntax
+
+```plaintext
+{[][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>];[>]O[H],[<][H][]}
+```
+
+> Note that the terminal bonding descriptors should still be included even if they are empty
+>
+> The same polymer can also be expressed with the mixed explicit and implicit representation:
+
+```plaintext
+[H]O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>];[<][H][]}
+```
+
+> To illustrate the use of end groups in branched polymers, consider the following hyperbranched polymer 
+>
+> <img src="images/descriptorDemo5.png" alt="alt text" style="zoom:40%;" />
+>
+> The polymer has exactly one end with boron end group, and all the other ends are capped by bromide. This end group pattern can be expressed by the implicit style or the mixed style
+
+```plaintext
+{[][<]c1cc([>])cc([>]c1);[<]Br,[>]B(O)O[]} (implicit)
+OB(O){[>][<]c1cc([>])cc([>]c1);[<]Br[]} (mixed)
+```
+
+> Note that the bromide end group cannot be specified explicitly because the number of bromide ends are not a fixed number but rather depends on the exact enumeration of the polymeric structure.
+
+
+
+As implied in the syntax, there are two equally valid options for reporting the end groups for a polymeric segment represented by a stochastic object:
+
+1. Explicit SMILES strings leading or trailing a stochastic object. 
+2. End groups specified within the list of end groups within the stochastic object.
+
+**Explicit End Groups**
+
+In the first case, the BigSMILES string is interpreted as the following figure,
+
+<img src="images/endGroup1.png" alt="alt text" style="zoom:30%;" />
+
+in which the terminal bonding descriptors are effectively attached to the atoms on the exterior SMILES string to serve as end groups. Each end group in this case corresponds to exactly one copy that caps one   open end of the polymeric fragment.
+
+Similar to the case for bonding descriptors found within repeat units, the SMILES bond symbol leading and trailing the stochastic objects are interpreted as the type of bond that should be formed when the atom on the explicit end group is joined with an atom on a repeat unit. Therefore, the leading and trailing bonds must be consistent with the bonding descriptors defined on the repeat units within the stochastic object.
+
+**Implicit End Group within Stochastic Object**
+
+In the second case, the end groups in the list are allowed to cap the open ends (atoms with unclosed bonding descriptors). If multiple end groups with identical bonding descriptors are present, the stochastic object implicitly include all possible molecular graphs. Note that all ends on the molecular fragment constructed by joining repeating units will be considered capped when end groups are specified. Only the ends without suitable end groups will be considered uncapped. 
+
+<img src="images/endGroup2.png" alt="alt text" style="zoom:30%;" />
+
+Note that unlike the explicit case, there may be multiple copy of each end group used in capping the polymeric fragment. This notation is especially useful in denoting the end groups of randomly branching polymers, in which the number of end groups required is not a fixed number, and therefore cannot be a priori specified.
+
+**Mixed Explicit and Implicit End Groups**
+
+In the case where a stochastic object with non-empty end group list also has explicit terminal end groups, the explicit end groups take precedence. That is, the explicit end groups will always be considered first to cap the open ends; after all explicit ends have been utilized, the remaining open ends are capped with the end groups defined within the stochastic object. This interpretation is illustrated by practical examples on the example panel.
+
+Depending on the context, the two representations may be more preferable than the other. 
+
+## Examples
+
+In this section, how BigSMILES could be used to represent several patterns commonly found in polymers is illustrated. Several useful features, such as the nesting of BigSMILES stochastic objects, would also be introduced.
+
+### Random Copolymer
+
+> Example of random copolymer 
+>
+> <img src="images/Demo2.png" alt="alt text" width="20%;" />
+
+```plaintext
+{[][$]CC(C#N)[$],[$]CC(c1ccccc1)[$][]}
+```
+
+In general, random copolymers can be easily encoded by having multiple repeat units within the same stochastic object. 
+
+<img src="images/pattern1.png" alt="alt text" style="zoom:30%;" />
+
+### Block Copolymer
+
+> Example of block copolymer 
+>
+> <img src="images/Demo3.png" alt="alt text" width="25%;" />
+
+```plaintext
+{[][<]OCC[>][<]}{[>][<]OC(C)C[>][]}
+```
+
+> Note that in this case, the "end group" for each of the polymeric segment is another polymer segment.
+
+Block copolymers can be constructed by having more than one stochastic objects in sequence.
+
+<img src="images/pattern2.png" alt="alt text" style="zoom:30%;" />
+
+Since syntax-wise stochastic objects behave just like atoms, multiple blocks can be readily represented by concatenating multiple stochastic objects.
+
+### Alternating Polymer
+
+> Example of  alternating copolymer 
+>
+> <img src="images/Demo4.png" alt="alt text" width="30%;" />
+
+```plaintext
+{[][<]C(=O)c1ccc(cc1)C(=O)[<],[>]OCCO[>][]}
+```
+
+Alternating polymer can be constructed by having two repeating units, with one unit containing only the `<` descriptor, and the other containing only the `>` descriptor.
+
+<img src="images/pattern3.png" alt="alt text" style="zoom:30%;" /> 
+
+### Branched Polymer
+
+> Polyethylene with branching reads
+
+```plaintext
+{[][$]CC[$],[$]CC([$])[$][]}
+```
+
+Branched polymer can be obtained by including repeat units with more than two bonding descriptors.
+
+<img src="images/pattern4.png" alt="alt text" style="zoom:30%;" />
+
+### Polymer Graft
+
+> Example of polymer graft
+>
+> <img src="images/Demo5.png" alt="alt text" width="20%;" />
+
+```plaintext
+{[][$]CC(C)(C)[$],[$]CC(c1ccc(cc1)C{[$][$]CC(C)(C(=O)OC)[$][$]}Br)[$][]}
+```
+
+Polymer graft can be encoded by including repeat units that contain macromolecular pendant groups.
+
+<img src="images/pattern5.png" alt="alt text" style="zoom:30%;" />
+
+Since syntax-wise stochastic objects behave just like atoms, the graft can be represented as a stochastic object nested within the outer stochastic object.
+
+### Macromonomer
+
+> Example of polyurethane with oligomer chain extenders
+>
+> <img src="images/Demo6.png" alt="alt text" width="55%;" />
+
+```plaintext
+{[][>]C(=O)Nc1ccc(C)c(c1)NC(=O)[>],[<]OCC{[<][>]OCC[<][>]}O[<],[<]OCCCO[<][]}
+```
+
+Polymer synthesized from macromonomers can be easily represented by including repeat units that contains stochastic objects.
+
+<img src="images/pattern6.png" alt="alt text" style="zoom:30%;" />
+
+Since syntax-wise stochastic objects behave just like atoms, the macromonomer can be represented as a stochastic object nested within the outer stochastic object.
+
+## Simplifications and Abbreviations
+
+To increase the readability of BigSMILES strings, several simplification schemes are introduced on top of the BigSMILES syntax. It should be noted that these are considered simplified conventions, and should not be considered as part of the formal syntax of the BigSMILES line notation.
+
+### Simplification for AA-Type Descriptors
+
+> The simplification for AA-type descriptor is especially useful for vinyl polymers. For instance, the poly(ethylene-butene) example can be simplified into
+
+```
+{[][$]CC[$],[$]CC(CC)[$][]} (full)
+{[]CC,CC(CC)[]} (simplified)
+```
+
+In the case when a stochastic object:
+
+1. contains only AA-type bonding descriptors (no AB-type)
+2. contains only one AA-type descriptor
+3. the bond order associated with the descriptor is 1
+4. every repeat unit has at least two bonding descriptors
+5. the BigSMILES string for every repeat unit is written so that the string starts with a descriptor and ends with a descriptor
+
+then the descriptors at the ends of the repeat units can be omitted. This convention can greatly simplify the expression for vinyl polymers.
+
+### Simplification for AB-Type Descriptors
+
+> The simplification for AB-type descriptor is especially useful for polymers such as poly(amino acid). For instance, a glycine alanine copolymer, the simplification reads
+>
+> <img src="images/descriptorDemo4.png" width="30%;" />
+
+```plaintext
+O{[>][<]C(=O)C(C)N[>],[<]C(=O)CN[>][<]} (full)
+O{[>]C(=O)C(C)N,C(=O)CN[<]} (simplified)
+```
+
+> However, it should be noted that if the original polymer is written as
+
+```plaintext
+{[<][<]C(=O)C(C)N[>],[<]C(=O)CN[>][>]}O (cannot be simplified)
+```
+
+> then this BigSMILES string cannot be simplified because condition 7 is not satisifeed. To express the polymer under this end group configuration, the repeat units must be "flipped":
+
+```plaintext
+{[<][>]NC(C)C(=O)[<],[>]NCC(=O)[<][>]}O (full)
+{[<]NC(C)C(=O),NCC(=O)[>]}O (full)
+```
+
+In the case when a stochastic object:
+
+1. contains only AB-type bonding descriptors
+2. contains only one AB-type descriptor
+3. the bond order associated with the descriptor is 1
+4. every repeat unit has exactly one descriptor of `<` type and one of `>` type
+5. the BigSMILES string for every repeat unit is written so that the string starts with a descriptor and ends with a descriptor
+6. every repeat unit starts with the same descriptor (all starts with `>` or all starts with `<`) and ends with the other descriptor
+7. at least one terminal bonding descriptor is specified, and the terminal descriptors correspond to the configuration in which the repeat units can be directly concatenated with the explicit end groups in the given orientation (without flipping).
+
+then the descriptors at the ends of the repeat units can be omitted. This convention can greatly simplify the expression for polymers such as poly(amino acids). 
+
+### Fragment Name Definition Notation
+
+> The fragment name definition can be useful for polymers with complex pendant groups. For example, the readability of the BigSMILES encoding could be greatly increased for the following polymer by replacing the complex side group
+>
+> <img src="images/Fragment1.png" width="30%;" />
+
+```plaintext
+{[][$]CC(C)([#R])[$][]}.{#R=C(=O)OCC12CC(C3)CC(C1)CC3C2}
+```
+
+> The fragment name definition is also useful when there are repetitive structures within the polymer
+>
+> <img src="images/Fragment2.png" width="40%;" />
+
+```plaintext
+C([#Arm])([#Arm])([#Arm])[#Arm].{#Arm=CO{[<][>]CCO[<][>]}}
+```
+
+In some cases, it might be more beneficial to have some portions of the BigSMILES representations be replaced by more abstract but compact proxies, for example, the names of repeating units. This is especially helpful when the structure is complex, and the resulting BigSMILES representation becomes long. 
+
+To facilitate understanding, a definition of molecular fragments that associate user-defined names with partial BigSMILES strings is allowed in BigSMILES. Within a BigSMILES string, a segment of the string can be replaced by a placeholder object with the following syntax:
+
+`'[' + '#' + fragment_name + ']'`
+
+The `fragment_name` should consist of only printable ASCII symbols (excluding whitespace characters). 
+
+In principle, as long as stochastic objects entirely contained, any segment can be replaced by such placeholder. However, it is recommended that bonding descriptors (except for those bracketed within a stochastic object) not be replaced and be left explicit within a BigSMILES string to avoid misleading expressions.
+
+In general, the definition for each placeholder fragment name used within a BigSMILES string must be given at the end of the BigSMILES string. The syntax for such definition is:
+
+`'.' + '{' + '#' + fragment_name + '=' + BigSMILES_string + '}'`
+
+each fragment definition is enclosed within a pair of curly brackets, and each definition is separated by dots (`.`). Combining the placeholder and definition sections, the full syntax for the fragment name definition notation reads:
+
+`BigSMILES_string + '[' + '#' + fragment_name + ']' + BigSMILES_string + '.' + '{' + '#' + fragment_name + '=' + BigSMILES_string + '}'`
+
+The only exception for the need of such explicit definition is when the placeholder is one of the common repeat units that has been defined in this [repository](#). These fragment names can be used without further re-definition.
+
+## Ladder Polymers
+
+> To illustrate the usage of the multiatom bonding descriptor, consider the following example:
+>
+> <img src="images/multiatom1.png" width="20%;" />
+>
+> The polymer is a ladder polymer with two parallel AB type bonds between each repeating unit. 
+>
+> First, since the two B moieties always connect to the same repeat unit, the bonding descriptor for them share the same group id. Similarly, the bonding descriptors on the A moieties share the same group id. (Choose `group_id`=1 for A and 2 for B).
+>
+> Since the overall connectivity between the two groups are of the AB type, the outer descriptor type is denoted by the `<` and `>` symbols. Hence, the outer bonding descriptors have the form:
+
+```plaintext
+A([<1[Inner]1])R(A[<1[Inner]1])(B[>1[Inner]2])B[>1[Inner]2]
+```
+
+> For the inner layer, since individual atoms are also of AB-type, the inner bonding descriptor are also denoted by the  `<` and `>` descriptors. Therefore, the complete ladder bonding descriptor reads
+
+```plaintext
+A([<1[<1]1])R(A[<1[<1]1])(B[>1[>1]2])B[>1[>1]2]
+```
+
+> To provide more illustrations on how the inner layer should be written, consider another example:
+>
+> <img src="images/multiatom2.png" width="20%;" />
+>
+> In this case, instead of having AB type bonds, the ladder polymer have a overall AA type connectivity in which the repeating units can be flipped arbitrarily. Owning to this nature, the outer descriptor type for this polymer is `$`, and the outer descriptors read
+
+```plaintext
+A([$1[Inner]1])R(A'[$1[Inner]1])(A[$1[Inner]2])A'[$1[Inner]2]
+```
+
+> Now, the inner layer descriptor depends on the exact connectivity pattern between the repeating units. Consider the three different cases:
+>
+> 1. Connections between A-A, A-A’, and A’-A’ are all allowed
+> 2. Connections between A-A and A’-A’ are allowed, but A-A’ is not allowed
+> 3. Only connection between A-A’ is allowed, connections between A-A, A’-A’ not allowed
+>
+> For the first case, the connectivity can be described by a single AA type connection among the group of A and A', leading to the overall bonding descriptor
+
+```plaintext
+A([$1[$1]1])R(A'[$1[$1]1])(A[$1[$1]2])A'[$1[$1]2]
+```
+
+> For the second case, As and A's can still form AA type connections within themselves, but cross-connections are disallowed. Therefore, the inner layer must be described by two orthogonal AA type connections, leading to the BigSMILES
+
+```plaintext
+A([$1[$1]1])R(A'[$1[$2]1])(A[$1[$1]2])A'[$1[$2]2]
+```
+
+>  For the last case, A and A' form conjugate AB type bonds; therefore, the inner layer reads
+
+```plaintext
+A([$1[<1]1])R(A'[$1[>1]1])(A[$1[<1]2])A'[$1[>1]2]
+```
+
+The BigSMILES syntax up to this point assumes that neighboring repeating units are always connected through a single pair of atoms. However, for some materials, such as ladder polymers, this condition does not hold. To represent ladder polymers or other polymers with multiple connections between a single pair of repeating units, the bonding descriptors are nested by the following syntax:
+
+`ladder_bond_descriptor = '[' + outer_descriptor + '[' + inner_descriptor + ']' + group_id + ']'`
+
+The bonding descriptor for multiatom connections is composed of four components:
+
+1. the outmost square brackets,
+2. the `group_id` of multi-atom bonds, an integer label for specifying the grouping of bonding descriptors within a repeat unit
+3. `outer_descriptor`, the outer bonding descriptor with syntax 
+   * `outer_descriptor = outer_descriptor_type + outer_descriptor_id `
+4. `inner_descriptor`, the inner bonding descriptor with syntax
+   * `inner_descriptor = inner_descriptor_type + inner_descriptor_id `
+
+### Grouping of Bonding Descriptors and the Outer Bonding Descriptor
+
+To describe multi-atom bonds, the atoms with bonding descriptors within the same repeat unit must first be divided into groups. Each group corresponds to the collection of atoms that always connect to the same neighboring repeating unit. The grouping is specified by the `group_id` parameter. Note that since `group_id` is used to group atoms/descriptors within the same repeat unit, the labels can be reused for groups on distinct repeating units. 
+
+Once the grouping is established, dimers and higher order fragments could be constructed by joining multiple repeat units. To describe the connectivity patterns that are permissible, the *outer* bonding descriptor is used. This outer bonding descriptor is analogous to the bonding descriptor found in normal non-ladder connections. The only difference between the ladder and non-ladder bonding descriptors is that the non-ladder descriptors describe the connectivity pattern of a single bond between two repeat units, whereas the ladder version describes the connectivity pattern of a group of bonds between two repeat units. It should be emphasized that since the entire group are collectively described by the same outer descriptor, individual bonding descriptors within the same group should always share the same outer descriptor type and the same outer descriptor id. 
+
+### Inner Bonding Descriptor
+
+While the outer bonding descriptor specified how group of bonds are established between repeat units, how each individual atom within the two groups are actually connected to form a proper ladder bond have yet to be specified. This information is encoded within a second bonding descriptor: the inner bonding descriptor. Similar to the outer descriptor, the inner bonding descriptor is also given by the same syntax as the usual bonding descriptor.
+
+<aside class="warning">
+The terminal bonding descriptor syntax has been updated and is different from the <a href="https://pubs.acs.org/doi/10.1021/acscentsci.9b00476">version 1.0 presented in ACS Central Science</a>.
+</aside>
+
+# Special Topics
+
+## Tacticity
+
+> To demonstrate how this notation can be used to specify tacticity, consider a polypropylene (PP) with pure head-to-tail configuration as example. 
+>
+> Atactic PP reads
+
+```plaintext
+CC{[>][<]CC(C)[>][<]}CC(C)=C
+```
+
+> Meanwhile, isotactic PP synthesized with Ziegler-Natta catalyst TiCl<sub>3</sub> and Al(C<sub>2</sub>H<sub>5</sub>)<sub>2</sub>Cl and terminated through β-hydrogen elimination or β-hydrogen transfer to monomer is represented by 
+
+```plaintext
+C{[>][<]C[C@@H](C)[>][<]}CC(C)=C
+```
+
+> whereas syndiotactic PP synthesized with VCl<sub>4</sub> and Al(C<sub>2</sub>H<sub>5</sub>)<sub>2</sub>Cl as catalyst reads
+
+```plaintext
+CC{[>][<]C[C@@H](C)C[C@H](C)[>];[<]C=CC,[<]C[C@H](C)C=CC[]}
+```
+
+> In the last example, the polymer is assumed to terminate through α-hydrogen elimination or α-hydrogen transfer to monomer, which leads to two possible end groups. The first possibility being that the polymer has an overall even number of carbon atoms along the backbone, whereas the second corresponds to the polymer having odd number of carbon atoms.
+
+Stereochemistry and chirality is supported by the `@` and `@@` modifiers provided by SMILES. 
+
+In general, the tacticity of a polymer include the following cases:
+
+1. Atactic:  
+   The polymer is assumed to be atactic if no explicit chirality specification is provided. 
+2. Isotactic:  
+   To specify an isotactic polymeric segment, the repeat unit should be modified with either the `@` or `@@` symbols to enforce the stereochemistry. For instance, the repeat unit of the following polymer reads  
+   <img src="images/tacticity1.png" width="20%;" />  
+   `[<]C[C@@H](C)[>]`  
+   whereas its stereoisomer reads  
+   <img src="images/tacticity2.png" width="20%;" />  
+   `[<]C[C@H](C)[>]`   
+3. Syndiotactic:  
+   To specify an syndiotactic polymer, the correlation between adjacent repeat units must be specified. This could be achieved by using the dimer of repeat units, or dyads, as the basic repeat unit instead:  
+   <img src="images/tacticity3.png" width="20%;" />  
+   `[<]C[C@@H](C)C[C@H](C)[>]`
+
+Note that here the `<` ,`>` bonding descriptors are used instead of `$` to restrict flipping of the orientation of the repeating units; this effectively specifies a purely head-to-tail configuration. 
+
+## Geometric Isomers
+
+SMILES denote the geometric isomers adjacent to double bonds with the `\` and `/` bonding symbols. In BigSMILES, these notations are used to describe the exact structure for isomers.
+
+Consider poly(ethyne), the polymer can be in one of the three states:
+
+1. Unspecified isomeric state:  
+   If the isomeric state is not explicitly known, the normal single bonds are used to indicate that the isomeric state is unspecified  
+   `[$]-C=C-[$]`
+2. Trans isomer:  
+   <img src="images/trans.png" width="30%;" />  
+   The trans isomer can be denoted by the following repeat unit  
+   `     [$1]/C=C/[$1]  ` or `     [$1]\C=C\[$1]  `
+3. Cis isomer:  
+   <img src="images/cis.png" width="30%;" />  
+   The cis isomer can be denoted by the following repeat unit  
+   `     [$1]\C=C/[$1]  ` or `     [$1]/C=C\[$1]  `
+
+As illustrated in the following figure, since in BigSMILES the process of concatenating two atoms with matching bonding descriptors is equivalent to forming a connection between the corresponding molecular graphs, therefore, even for bonding descriptors with leading bonds that are not identical, for instance,
+
+`...C=C/[$]` and `[$]\C=C...`  
+
+or 
+
+`...C=C/[$]` and `[$]-C-C...`
+
+the bonding descriptors are still considered consistent.
+
+<img src="images/geometric1.png" style="zoom:30%;" />
+
+In general, as long as the bond order for the bonds leading to the bonding descriptors matches, they are considered consistent.
+
+## Polyelectrolytes
+
+> Another example of a charged polymer is the quaternized 4-vinylpyridine polymer
+>
+> <img src="images/polyelectrolyte1.png" width="25%;" />
+>
+> synthesized by Obermeyer et al. (*Soft Matter*,*12*(15), 3570-3581, **2016**). 
+>
+> The BigSMILES for the polymer reads
+
+```plaintext
+{[][$]CC(c1ccncc1)[$],[$]CC(c1cc[n+](C)cc1)[$].[I-][]}
+```
+
+In SMILES, there is no explicit notation for ionic bonds, and ionic species are expressed as separate compounds and joined together into a SMILES string with the dot `.` symbol. For example, sodium chloride reads
+
+`[Na+].[Cl-]`
+
+To denote polyelectrolytes, the repeat units are written in a similar fashion. For instance, the repeat unit for poly(methacrylic acid, sodium salt) reads
+
+`[$]CC(C)(C(=O)[O-])[$].[Na+]`
+
+and the polymer reads
+
+`{[][$]CC(C)(C(=O)[O-])[$].[Na+][]}`
+
+By explicitly including both the charged repeat unit and its counter-ion within the BigSMILES repeat unit, the overall neutrality of the whole polymer is guaranteed.
+
+Similarly, for other strong polyelectrolytes, such as poly(sodium styrene sulfonate), the BigSMILES repeat unit is written in a similar way
+
+`{[][$]CC(c1ccc(S(=O)(=O)[O-])cc1)[$].[Na+])[]}`
+
+For weak polyelectrolytes, the partial dissociation can be represented by including both the dissociated and undissociated repeating units within the list; for example, poly(acrylic acid) can be represented as
+
+`{[][$]CC(C(=O)O)[$],[$]CC(C(=O)[O-])[$].[OH3+][]}`
+
+By explicitly including the different repeating unit-counter-ion pairs, this representation can also be used to denote polyelectrolytes with multiple counter-ions. 
+
+## Branched Polymers and Polymer Networks
+
+> In general, a branched polymer synthesized with A and B type precursors will read
+>
+> <img src="images/branch4.png" width="70%;" />
+
+```plaintext
+{[][<][#A][#R][#A][<],[>][#B][#R']([#B][>])([#B][>])[#B][>][]}
+```
+
+> where `#A`, `#B`, `#R` and `#R'` are placeholders for reactive and linking chemical fragments.
+>
+> Examples of this type of polymer include end-linked elastomers, gels and epoxide networks.
+>
+> As discussed in detail in the end group section, for these randomly branched networks, it is necessary to include implicit end groups to fully cap the entire polymer. 
+
+```plaintext
+{[][<][#A][#R][#A][<],[>][#B][#R']([#B][>])([#B][>])[#B][>];[>][#E1],[<][#E2][]}
+```
+
+> where `#E1` and `#E2` are placeholders for end groups attached to `#A` and `#B` ends.
+
+By having repeating units with more than two bonds to other units, the syntax of BigSMILES naturally extends from linear polymers to branched polymers and polymer networks. For example, a branched poly(ethylene) can be encoded as
+
+`{[][$]CC[$],[$]CC([$])[$],[$]C([$])C([$])[$][]}`
+
+where the first repeat unit represent the linear unit, and the latter two are repeat units with functionality of three and four.
+
+Similar syntax can also be used to represent other branching structures, such as hyperbranched polymers,  dendrimers or even polymer networks and gels. For instance, a polyethylenimine hyperbranched polymer reads
+
+<img src="images/branch3.png" width="50%;" />
+
+`{[][<]CCN([>])[>][]}`
+
+Moreover, polymer networks can also be encoded by with similar strategy. For example, poly(1,3-butadiene) rubber vulcanized by sulfur bridges reads
+
+<img src="images/branch5.png" width="70%;" />
+
+`{[][$]CC=CC[$],[$]CC([<])C([<])C[$],[>]{[$][$]S[$][$]}[>][]}`
+
+## Dynamic, Topological and Physical Bonds
+
+The same syntax can also be used to represent several other types of polymer networks or structures beyond the scope of traditional chemical networks, including vitrimers, double network gels (DN gels), poly(metal organic cage) (polyMOC) and poly(metal organic framework) (polyMOF) networks. 
+
+### Vitrimers
+
+For vitrimers, since all bonds between the components are covalent, and local connectivity rules are well-defined, they can be simply encoded with the same syntax as usual chemical networks shown in the previous sections. 
+
+However, it should be noted that the dynamic natures of the bonds cannot be captured by the static representation.
+
+### Double Network (DN) Gels
+
+For DN gels, the coexisting molecules that are not covalently bonded can be represented by a set of repeating units with orthogonal bonding types. For example, denoting the repeating unit of the first network by A and the second by B, a simple (yet unrealistic) DN gel can be encoded as
+
+`{[][$1]A([$1])[$1],[$2]B([$2])[$2][]}`
+
+Meanwhile, it is worth noting that the topological bonds are beyond the current design of BigSMILES language, and cannot be faithfully reflected.
+
+### PolyMOCs and PolyMOF
+
+> As an example, consider the M<sub>2</sub>L<sub>4</sub> polyMOC synthesized by Zhukhovitskiy et al. 
+>
+> <img src="images/network1.png" width="70%;" />
+>
+> can be represented by the BigSMILES
+
+```plaintext
+{[][>]COC(=O){[$][$]COC[$][$]}C(=O)OC[>],
+c1([<])cc([#L]2)cc([#L]3)c1.c4([<])cc([#L]5)cc([#L]6)c4.
+c7([<])cc([#L]8)cc([#L]9)c7.C%10([<])cc([#L]%11)cc([#L]%12)c%10.
+[Pd++]258%11.[Pd++]369%12}.{#L=c(c1)cccn1}
+```
+
+If the single bond notation is extended to incorporate also the coordinate bonds, metal organic networks could also be encoded using the BigSMILES. 
+
+However, while BigSMILES could capture the local connectivity pattern for MOFs, the local nature of BigSMILES means that large scale ordering, such as the crystalline structure of MOFs, could not be captured. Furthermore, the SMILES `@` and `@@` syntax for chirality is insufficient to fully specify the three-dimensional structure of a metal center that has more than four ligands.
+
+### Other Networks
+
+Besides the discussed networks, there are still many other types of polymer networks, including physical networks or topological network such as the slide-ring gel. Currently, BigSMILES do not support most of these non-covalent networks. However, as SMILES is further developed, support for these more complex structures may be possible in future revisions of BigSMILES.
+
+## Macrocycles
+
+> Consider a polystyrene ring synthesized through alkyne azide click chemistry, since rings and cycles within repeating units do not extend beyond a single repeating unit, the macrocycle associated with the ring polymer can be treated with the usual SMILES ring closure syntax:
+>
+> <img src="images/macrocycle1.png" width="70%;" />
+
+```plaintext
+S1C(c2ccccc2){[$][$]CC(c1ccccc1)[$][$]}C(=O)OC(=C3)N=NN3CC(O)COC(=O)C(C)C1
+```
+
+> Note that the integer identifier that was used within the repeating units for ring closure is considered to be independent of any ring closure ID that was used in other parts of the BigSMILES string.
+
+### Well-defined Macrocycles
+
+For well-defined macrocycles, such as the cycle structures in ring polymers, the macrocycles are encoded using the usual syntax for describing cycles in SMILES. This is illustrated by the first example in the right panel.
+
+### Implicit Ring Polymers
+
+> Consider a ring polymer synthesized with ring expansion metathesis polymerization (REMP)
+>
+> <img src="images/macrocycle2.png" width="30%;" />
+>
+> To denote a ring polymer, the ring notation must be explicitly invoked
+
+```plaintext
+C1CCC{[$1][$1]=CCCCCCCC=[$1][$1]}CCCC1
+```
+
+> However, the uncapped linear polymer 
+
+```plaintext
+{[][$1]=CCCCCCCC=[$1][]}
+```
+
+> also implicitly include similar ring polymers.
+
+It should be emphasized that for a linear polymer of the form 
+
+`{[][$][#RepUnit][$][]}`
+
+although there is no explicit reference to any cycle, since a ring polymer is also a valid outcome of the connectivity pattern, the molecular ensemble would implicitly include both the uncapped linear fragments and also ring polymers without any open ends. This is meant to reflect the potential ring-chain formation competition that naturally occurs during polymerization. 
+
+### Random Cycles
+
+For stochastic objects representing branching polymers, products with random cyclization are also included within the ensemble of possible structures. This looping structure is currently not explicitly presented within the BigSMILES representation, because each cycle requires indexing in the SMILES language, and it is impossible to fully track such random cyclization explicitly. Rather, the possibility of this type of random macrocycle formation is implicitly included through expressing the rules of connectivity that allow for the enumeration of these macrocycles.
+
+
+
+# Version History
+
+## version-1.1
+
+### Date
+
+November 11, 2019
+
+### Major Updates
+
+Made changes to the bonding descriptor syntax:
+
+1. Changed bonding descriptor from `descriptor_type + bond_type + descriptor_id` to `'[' + descriptor_type +  descriptor_id  +  ']'` and moves bonding symbol out of the descriptors
+   * Made descriptors completely identical to atoms in terms of syntax
+   * Resolve incompatibility with SMILES quadruple bonds `$`
+   * Resolve potential conflict with reaction SMILES `>`
+2. Changed ladder bonding descriptor syntax: added `group_id` 
+   * Original syntax may lead to ambiguous semantics
+
+3. Changed terminal bonding descriptor syntax
+
+   * To eliminate ambiguity, the terminal bonding descriptors are always required, even if it is empty 
+
+     
+
+## version-1.0
+
+### Date
+
+September 12, 2019
+
+### Major Update
+
+BigSMILES line notation published on [ACS Central Science](https://pubs.acs.org/doi/10.1021/acscentsci.9b00476)
\ No newline at end of file
diff --git a/docs/bigsmiles/overview.md b/docs/bigsmiles/overview.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/bigsmiles/references.md b/docs/bigsmiles/references.md
new file mode 100644
index 0000000..2304ac0
--- /dev/null
+++ b/docs/bigsmiles/references.md
@@ -0,0 +1,16 @@
+---
+
+## Old BigSMILES documentation
+[Learn more about BigSMILES Notation](https://olsenlabmit.github.io/BigSMILES/docs/line_notation.html#the-bigsmiles-line-notation)
+
+
+
+## SMILES(Simplified Molecular Input Line Entry System)
+* [Daylight](https://www.daylight.com/dayhtml/doc/theory/theory.smiles.html)
+* [openSMILES](http://opensmiles.org/opensmiles.html)
+* [github](https://github.com/opensmiles)
+* [SMILES+](https://github.com/IUPAC/IUPAC_SMILES_plus)
+
+## Reaction SMILES:
+* [Daylight Summer School](https://www.daylight.com/meetings/summerschool01/course/basics/smirks.html)
+* [Daylight SMIRKS](https://www.daylight.com/dayhtml/doc/theory/theory.smirks.html)
diff --git a/docs/bigsmiles/specification.md b/docs/bigsmiles/specification.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/bigsmiles/stereochemistry.md b/docs/bigsmiles/stereochemistry.md
new file mode 100644
index 0000000..537763a
--- /dev/null
+++ b/docs/bigsmiles/stereochemistry.md
@@ -0,0 +1,24 @@
+
+
+
+## E/Z
+
+priority rules:
+
+1) Higher atomic number precedes lower. 
+2) Higher atomic mass precedes lower (in the case of isotopes)
+   * Use exact isotopic mass when an isotope is indicated, and use average atomic weight when it is not
+3) 'E' precedes 'Z'
+4) Chiral stereogenic units precede pseudoasymmetric stereogenic units, and these precede nonstereogenic units
+5) “r” precedes “s” 
+6) An undifferentiated reference node has priority over any other undifferentiated node.
+
+### References
+* Nomenclature of Organic Chemistry
+  * https://books.rsc.org/books/monograph/180/Nomenclature-of-Organic-Chemistry
+  * https://doi.org/10.1039/9781849733069
+* Algorithmic Analysis of Cahn–Ingold–Prelog Rules of Stereochemistry: Proposals for Revised Rules and 
+a Guide for Machine Implementation
+  * https://pubs.acs.org/doi/full/10.1021/acs.jcim.8b00324
+* Jmol SMILES and Jmol SMARTS: specifications and applications
+  * https://jcheminf.biomedcentral.com/articles/10.1186/s13321-016-0160-4
\ No newline at end of file
diff --git a/docs/developers/developers.md b/docs/developers/developers.md
new file mode 100644
index 0000000..bc119eb
--- /dev/null
+++ b/docs/developers/developers.md
@@ -0,0 +1,34 @@
+# Developers
+
+---
+
+## Goals of the project
+
+The main goal of this project is to provide a lightweight parsing and validation of any SMILES/BigSMILES string that
+can be the building off point for other cheminformatics.
+
+
+With that said; this package will also try to support additional representations and algorithms related to 
+chemiformatics as optional features (and optional dependencies).
+
+
+---
+
+## Want to request a feature?
+Submit as an `issue` and use GitHub labels `enhancement`.
+
+
+
+## Want to develop a feature?
+Help is always welcomed.
+Submit as an `issue` and use GitHub labels `enhancement`.
+We can provide guidance before you start your work!
+
+
+
+## Found a bug in the code? 
+
+(there are probably a few <span> &#128557;</span>  )
+
+Submit as an `issue` and use GitHub labels `bug`.
+Solutions are also welcomed (either through quick code blocks or pull requests.)
diff --git a/docs/extras/change_log.md b/docs/extras/change_log.md
new file mode 100644
index 0000000..fdb3672
--- /dev/null
+++ b/docs/extras/change_log.md
@@ -0,0 +1,3 @@
+# Change Log
+
+
diff --git a/docs/extras/license.md b/docs/extras/license.md
new file mode 100644
index 0000000..b079216
--- /dev/null
+++ b/docs/extras/license.md
@@ -0,0 +1,7 @@
+
+## 3 - Clause BSD License
+
+```
+--8<-- "LICENSE.txt"
+```
+[comment]: https://facelessuser.github.io/pymdown-extensions/extensions/snippets/
\ No newline at end of file
diff --git a/docs/img/poly_cloud.png b/docs/img/poly_cloud.png
new file mode 100644
index 0000000..f66fbe1
Binary files /dev/null and b/docs/img/poly_cloud.png differ
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..5711ff3
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,21 @@
+# BigSMILES
+
+???+ warning 
+    The BigSMILES package and documentation is still being built. So things may change or be incomplete.
+
+    Please report bugs if you find them!
+
+
+
+
+This site contains the project documentation for the 'BigSMILES' project.
+
+
+BigSMILES is a structurally based line notation based on the highly successful 
+SMILES (simplified molecular-input line-entry system) representation.  
+It provides extensions over regular SMILES, which can only represent molecules with given deterministic 
+molecular structures, and support the representation of molecules that are characterized by a distribution of 
+distinct molecular structures instead of a single well-defined structure. This feature is designed specifically to 
+reflect the stochastic nature of polymer molecules. Since polymers are macromolecules that usually have large molecular
+weights, the line notation is termed BigSMILES.
+
diff --git a/docs/quickstart/overview.md b/docs/quickstart/overview.md
new file mode 100644
index 0000000..79af4da
--- /dev/null
+++ b/docs/quickstart/overview.md
@@ -0,0 +1,27 @@
+# Overview:
+
+---
+
+This part of the project documentation focuses on help newcomers with getting started.
+
+** Step 1: Installation **
+
+
+!!! success "Installation"
+    To install BigSMILES use the following command:
+    
+    `pip install bigsmiles`
+    
+    Or go to:
+    
+    [bigsmiles · PyPI](https://pypi.org/project/bigsmiles/)
+    
+    
+    Or visit GitHub:
+    
+    [bigsmiles · GitHub](https://github.com/dylanwal/BigSMILES)
+
+
+** Step 2: Lesson 1 **
+
+Click 'lesson 1' on the navigation in the right sidebar.
diff --git a/docs/quickstart/quickstart.md b/docs/quickstart/quickstart.md
new file mode 100644
index 0000000..ca19ac7
--- /dev/null
+++ b/docs/quickstart/quickstart.md
@@ -0,0 +1,12 @@
+
+
+suggested shortening of `import bigsmiles` --> `import bigsmiles as bs`
+
+
+```python
+import bigsmiles
+
+polymer_string = "CC{[>][<]CC(C)[>][<]}CC(C)=C"
+polymer = bigsmiles.BigSMILES(polymer_string)
+print(bigsmiles)  # "CC{[>][<]CC(C)[>][<]}CC(C)=C"
+```
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
new file mode 100644
index 0000000..f61381f
--- /dev/null
+++ b/docs/reference/configuration.md
@@ -0,0 +1,22 @@
+
+
+### Colored outputs
+```python
+import bigsmiles
+
+bigsmiles.Config.color_output = True
+```
+
+BigSMILES: (html and letex verions shown)
+
+CC
+<span style="color:red">{</span>
+<span style="color:green">[>]</span>
+<span style="color:green">[<]</span>
+CC(C)
+<span style="color:green">[>]</span>
+<span style="color:green">[<]</span>
+<span style="color:red">}</span>
+CC(C)=C
+
+
diff --git a/docs/reference/constructor/constructor.md b/docs/reference/constructor/constructor.md
new file mode 100644
index 0000000..ce57cae
--- /dev/null
+++ b/docs/reference/constructor/constructor.md
@@ -0,0 +1,65 @@
+---
+
+
+## exit 
+
+!!! warning
+    This should be run at the end of any BigSMILES construction.
+
+::: bigsmiles.constructors.constructor.exit_construction
+
+
+## build it step-by-step
+
+::: bigsmiles.constructors.constructor.add_atom
+
+::: bigsmiles.constructors.constructor.add_bond
+
+::: bigsmiles.constructors.constructor.add_ring_by_index
+
+::: bigsmiles.constructors.constructor.add_ring_from_atoms
+
+::: bigsmiles.constructors.constructor.add_bonding_descriptor_atom
+
+::: bigsmiles.constructors.constructor.add_bond_atom_pair
+
+::: bigsmiles.constructors.constructor.add_bond_bonding_descriptor_pair
+
+::: bigsmiles.constructors.constructor.open_branch
+
+::: bigsmiles.constructors.constructor.close_branch
+
+::: bigsmiles.constructors.constructor.open_stochastic_object
+
+::: bigsmiles.constructors.constructor.open_stochastic_object_fragment
+
+::: bigsmiles.constructors.constructor.open_stochastic_object_fragment_with_bond
+
+::: bigsmiles.constructors.constructor.close_stochastic_object
+
+::: bigsmiles.constructors.constructor.open_stochastic_fragment
+
+::: bigsmiles.constructors.constructor.close_open_stochastic_fragment
+
+::: bigsmiles.constructors.constructor.close_stochastic_fragment
+
+
+
+## build it in chunks
+
+
+
+::: bigsmiles.constructors.constructor.append_bigsmiles_fragment
+
+::: bigsmiles.constructors.constructor.attach_bigsmiles_branch
+
+::: bigsmiles.constructors.constructor.insert_atom_and_bond
+
+::: bigsmiles.constructors.constructor.insert_atom_into_bond
+
+::: bigsmiles.constructors.constructor.add_bonding_descriptor_bond_via_index
+
+::: bigsmiles.constructors.constructor.add_stochastic_fragment
+
+::: bigsmiles.constructors.constructor.add_bigsmiles_as_stochastic_fragment
+
diff --git a/docs/reference/constructor/constructor_reaction.md b/docs/reference/constructor/constructor_reaction.md
new file mode 100644
index 0000000..a3f5bf6
--- /dev/null
+++ b/docs/reference/constructor/constructor_reaction.md
@@ -0,0 +1,6 @@
+---
+
+::: bigsmiles.constructors.constructor_reaction.parse_reaction
+
+
+::: bigsmiles.constructors.constructor_reaction.split_chemical
diff --git a/docs/reference/constructor/constructor_str.md b/docs/reference/constructor/constructor_str.md
new file mode 100644
index 0000000..5859888
--- /dev/null
+++ b/docs/reference/constructor/constructor_str.md
@@ -0,0 +1,33 @@
+---
+
+::: bigsmiles.constructors.constructor_str.add_atom_str
+
+
+::: bigsmiles.constructors.constructor_str.add_bonding_descriptor_atom_str
+
+
+::: bigsmiles.constructors.constructor_str.add_bond_atom_pair_str
+
+
+::: bigsmiles.constructors.constructor_str.add_bond_bonding_descriptor_pair_str
+
+
+::: bigsmiles.constructors.constructor_str.open_stochastic_object_fragment_str
+
+
+::: bigsmiles.constructors.constructor_str.open_stochastic_object_str
+
+
+::: bigsmiles.constructors.constructor_str.open_stochastic_object_fragment_with_bond_str
+
+
+::: bigsmiles.constructors.constructor_str.close_stochastic_object_str
+
+
+::: bigsmiles.constructors.constructor_str.open_stochastic_fragment
+
+
+::: bigsmiles.constructors.constructor_str.close_open_stochastic_fragment_str
+
+
+::: bigsmiles.constructors.constructor_str.close_stochastic_fragment_str
\ No newline at end of file
diff --git a/docs/reference/constructor/constructor_tokens.md b/docs/reference/constructor/constructor_tokens.md
new file mode 100644
index 0000000..fb1226f
--- /dev/null
+++ b/docs/reference/constructor/constructor_tokens.md
@@ -0,0 +1,7 @@
+---
+
+::: bigsmiles.constructors.constructor_tokens.parse_bigsmiles_str
+
+::: bigsmiles.constructors.constructor_tokens.tokens_to_bigsmiles
+
+
diff --git a/docs/reference/constructor/overview.md b/docs/reference/constructor/overview.md
new file mode 100644
index 0000000..813b445
--- /dev/null
+++ b/docs/reference/constructor/overview.md
@@ -0,0 +1,59 @@
+---
+
+Constructors are used to build BigSMILES objects. There are three types of the constructor available:
+
+!!! info "Constructor"
+    Base constructor that requires full parse information.
+
+    **^^There are two approaches to building a BigSMILES object:^^**
+
+    * build it step-by-step
+
+        1. open branch 
+        2. add atoms 
+        3. close branch
+
+    * build it in chunks
+
+        1. build the branch in a separate BigSMILES object 
+        2. attach the branch at a specified position
+
+    ^^Examples^^
+
+    `add_atom(parent, symbol, isotope, stereo, hydrogens, charge, valence, class_, kwargs)`
+
+
+!!! info "Constructor String"
+    Wraps several of the base constructor constructors. Performs parsing of string inputs before passing values to 
+    the base constructor functions.
+    
+    Methods are appended with '_str' to denote the methods that accept strings.
+
+    ^^Examples^^
+
+    `add_atom(parent, symbol_str, kwargs)`
+
+
+
+!!! info "Constructor Tokens"
+    Converts a list[Tokens] into a BigSMILES leveraging the 'constructor string'. These function control more of 
+    the flow of constructing a BigSMILES; i.e., knowing when to call what constructor function given the situation.
+
+    ^^Example^^
+    
+    "C(C)C" --> [Token(TokenKind.Atom, "C"), Token(TokenKind.BranchStart, "("), Token(TokenKind.Atom, "C"),
+    Token(TokenKind.BranchEnd, ")"), Token(TokenKind.Atom, "C")]
+
+    "[13C@H+:1]" --> {"symbol": "C", "isotope": 13, "stereo": "@", "hydrogens": 1, "charge": 1, "class_": 1}
+
+
+## Tokenizer
+
+The tokenizer receives BigSMILES strings, Atom strings, or Bonding Descriptor strings and breaks it up into 
+the individual tokens or parses in to a dictionary of distinct values. 
+
+
+## Reaction
+
+The reaction tokenizer parses and tokenized the reaction string. 
+Molecules are constructed via `BigSMILES` --> `Constructor Tokens`.
diff --git a/docs/reference/constructor/tokenizer.md b/docs/reference/constructor/tokenizer.md
new file mode 100644
index 0000000..a5669fb
--- /dev/null
+++ b/docs/reference/constructor/tokenizer.md
@@ -0,0 +1,75 @@
+---
+
+## Regular Expression Patterns
+
+Tokenization is mainly done through regular expression patterns.
+
+!!! info "Regular expression Patterns"
+    Bond = '-|=|#|$'
+    
+    Atom = 'S|P|O|N|I|F|Cl|C|Br|B'
+    
+    Aromatic = 'o|c|n|p|s|b'
+    
+    AtomExtend = r'(?:\\[)(?P<isotope>[\\d]{1,3})?
+    (?P<element>o|c|n|p|s|b|Zr|Zn|Yb|Y|Xe|W|V|U|Ts|Tm|Tl|Ti|Th|Te|Tc|Tb|Ta|Sr
+    |Sn|Sm|Si|Sg|Se|Sc|Sb|S|Ru|Rn|Rh|Rg|Rf|Re|Rb|Ra|Pu|Pt|Pr|Po|Pm|Pd|Pb|Pa|P|Os|Og|O|Np|No|Ni|Nh|Ne|Nd|Nb|Na|N|Mt|Mo|Mn|Mg
+    |Md|Mc|Lv|Lu|Lr|Li|La|Kr|K|Ir|In|I|Hs|Ho|Hg|Hf|He|H|Ge|Gd|Ga|Fr|Fm|Fl|Fe|F|Eu|Es|Er|Dy|Ds|Db|Cu|Cs|Cr|Co|Cn|Cm|Cl|Cf|Ce
+    |Cd|Ca|C|Br|Bk|Bi|Bh|Be|Ba|B|Au|At|As|Ar|Am|Al|Ag|Ac)
+    (?P<stereo>@{1,2})?(?P<hydrogens>H[\\d]?)?(?P<charge>[-|\\+]{1,3}[\\d]?)?(?P<class_>:\\d{1,3})?(?:\\])'
+    
+    BranchStart = r'\('
+    
+    BranchEnd = r'\)'
+    
+    Ring = r'[\d]{1}'
+    
+    Ring2 = r'%[\d]{2}'
+    
+    BondEZ = r'/|\\'
+    
+    Disconnected = r"\."
+    
+    BondDescriptorLadder = r"\[[$<>][\d]\[[$<>][\d]?\][\d]?\]"
+    
+    BondDescriptor = r"\[[$<>][\d]?[\d]?\]"
+    
+    StochasticSeperator = r",|;"
+    
+    StochasticStart = r'\{'
+    
+    StochasticEnd = r'\}'
+    
+    ImplictEndGroup = r'\[\]'
+    
+    Rxn = r'>>|>'
+
+
+
+
+## Atom tokenizer
+
+::: bigsmiles.constructors.tokenizer.tokenize_atom_symbol
+
+
+---
+
+## Bonding Descriptor Tokenizer
+
+::: bigsmiles.constructors.tokenizer.tokenize_bonding_descriptor
+
+
+---
+
+## BigSMILES Tokenizer
+
+The tokenizer leverage python's regular expression to identify and label valid BigSMILE tokens.
+
+
+::: bigsmiles.constructors.tokenizer.tokenize
+
+
+::: bigsmiles.constructors.tokenizer.Token
+
+
+::: bigsmiles.constructors.tokenizer.TokenKind
\ No newline at end of file
diff --git a/docs/reference/data_structure/Atom.md b/docs/reference/data_structure/Atom.md
new file mode 100644
index 0000000..f640a7c
--- /dev/null
+++ b/docs/reference/data_structure/Atom.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.bigsmiles.Atom
\ No newline at end of file
diff --git a/docs/reference/data_structure/BigSMILES.md b/docs/reference/data_structure/BigSMILES.md
new file mode 100644
index 0000000..b26658f
--- /dev/null
+++ b/docs/reference/data_structure/BigSMILES.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.bigsmiles.BigSMILES
diff --git a/docs/reference/data_structure/Bond.md b/docs/reference/data_structure/Bond.md
new file mode 100644
index 0000000..c9ce81c
--- /dev/null
+++ b/docs/reference/data_structure/Bond.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.bigsmiles.Bond
\ No newline at end of file
diff --git a/docs/reference/data_structure/Bond_descriptor.md b/docs/reference/data_structure/Bond_descriptor.md
new file mode 100644
index 0000000..8171a81
--- /dev/null
+++ b/docs/reference/data_structure/Bond_descriptor.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.bigsmiles.BondDescriptor
\ No newline at end of file
diff --git a/docs/reference/data_structure/Bond_descriptor_atom.md b/docs/reference/data_structure/Bond_descriptor_atom.md
new file mode 100644
index 0000000..711b568
--- /dev/null
+++ b/docs/reference/data_structure/Bond_descriptor_atom.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.bigsmiles.BondDescriptorAtom
\ No newline at end of file
diff --git a/docs/reference/data_structure/Branch.md b/docs/reference/data_structure/Branch.md
new file mode 100644
index 0000000..fcfe0ae
--- /dev/null
+++ b/docs/reference/data_structure/Branch.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.bigsmiles.Branch
\ No newline at end of file
diff --git a/docs/reference/data_structure/Reaction.md b/docs/reference/data_structure/Reaction.md
new file mode 100644
index 0000000..75cb195
--- /dev/null
+++ b/docs/reference/data_structure/Reaction.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.reaction.Reaction
\ No newline at end of file
diff --git a/docs/reference/data_structure/Stochastic_fragment.md b/docs/reference/data_structure/Stochastic_fragment.md
new file mode 100644
index 0000000..1685d68
--- /dev/null
+++ b/docs/reference/data_structure/Stochastic_fragment.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.bigsmiles.StochasticFragment
\ No newline at end of file
diff --git a/docs/reference/data_structure/Stochastic_object.md b/docs/reference/data_structure/Stochastic_object.md
new file mode 100644
index 0000000..1482f20
--- /dev/null
+++ b/docs/reference/data_structure/Stochastic_object.md
@@ -0,0 +1,3 @@
+---
+
+::: bigsmiles.data_structures.bigsmiles.StochasticObject
\ No newline at end of file
diff --git a/docs/reference/data_structure/overview.md b/docs/reference/data_structure/overview.md
new file mode 100644
index 0000000..a7233fc
--- /dev/null
+++ b/docs/reference/data_structure/overview.md
@@ -0,0 +1,101 @@
+
+The BigSMILES will be parsed into an abstract syntax tree. There are 3 classes of nodes:
+
+* root node: `BigSMILES`
+* intermediate nodes: `StochasticObject`, `StochasticFragment`, `Branch`
+* leaf nodes: `BondDescriptorAtom`, `Atom`, `Bond`
+
+
+BigSMILE objects (`Atom`, `Bond`, `BondingDescriptor`, `Branch`, `StochasticFragment`, `StochasticObject`, `BigSMILES`) 
+only holds data. 
+
+
+!!! info
+    Creation of these objects are handled by the 'Constructor' methods.
+
+<b>
+The figure shows the data objects and links to other data objects. 
+The arrows point from 'parent' objects to 'child' objects. Meaning `Reaction` --> `BigSMILES` means that you will 
+find attributes in `Reaction` that are `BigSMILES` objects.
+
+</b>
+
+
+```mermaid
+classDiagram
+    
+    class Reaction {
+        reactants: [BigSMILES]
+        agents: [BigSMILES]
+        products: [BigSMILES]
+    }
+
+    class BigSMILES {
+        nodes: [Atom, Bond, Branch, StochasticObject]
+        atoms: [Atom]
+        bonds: [Bond]
+        rings: [Bond]
+    }
+    
+    
+    class StochasticObject {
+        nodes: [StochasticFragment]
+        bond_left: Bond
+        bond_right: Bond
+        bond_descriptors: BondDescriptor
+    }
+    
+    
+    class StochasticFragment {
+        nodes: [Atom, Bond, Branch, StochasticObject]
+        rings: [Bond]
+    }
+    
+    
+    class Branch {
+        nodes: [Atom, Bond, Branch, StochasticObject]
+    }
+    
+    
+    class BondDescriptorAtom {
+        BondDescriptor: descriptor
+        Bond: bond
+    }
+    
+    
+    class BondDescriptor {
+        instances: list[BondDescriptorAtom] 
+    }
+
+    
+    class Bond {
+        atom1: Atom
+        atom2 Atom
+    }
+    
+    class Atom {
+        bonds: list[Bond]: 
+    }
+
+    Reaction --|> BigSMILES
+    BigSMILES --|> Atom
+    BigSMILES --|> Bond
+    BigSMILES --|> Branch
+    BigSMILES --|> StochasticObject
+    StochasticObject --|> StochasticFragment
+    StochasticObject --|> BondDescriptor
+    StochasticFragment --|> BondDescriptorAtom
+    BondDescriptor --|> BondDescriptorAtom
+    StochasticFragment --|> Atom
+    StochasticFragment --|> Bond
+    StochasticFragment --|> Branch
+    StochasticFragment --|> StochasticObject
+    Branch --|> BondDescriptorAtom
+    Branch --|> StochasticObject
+    Branch --|> Bond
+    Branch --|> Atom
+    
+```
+
+
+
diff --git a/docs/reference/errors.md b/docs/reference/errors.md
new file mode 100644
index 0000000..23dc0c5
--- /dev/null
+++ b/docs/reference/errors.md
@@ -0,0 +1,15 @@
+
+
+::: bigsmiles.errors.BigSMILESError
+
+
+::: bigsmiles.errors.TokenizeError
+
+
+::: bigsmiles.errors.ConstructorError
+
+
+::: bigsmiles.errors.ValidationError
+
+
+
diff --git a/docs/reference/methods/overview.md b/docs/reference/methods/overview.md
new file mode 100644
index 0000000..3101130
--- /dev/null
+++ b/docs/reference/methods/overview.md
@@ -0,0 +1,4 @@
+# Methods
+
+---
+
diff --git a/docs/reference/methods/parse_tree.md b/docs/reference/methods/parse_tree.md
new file mode 100644
index 0000000..7ae02b1
--- /dev/null
+++ b/docs/reference/methods/parse_tree.md
@@ -0,0 +1,52 @@
+---
+
+::: bigsmiles.methods.display_methods.tree_to_string.tree_to_string
+
+
+## Example
+
+#### Code:
+```python
+import bigsmiles
+import bigsmiles.methods as bs_methods
+
+polymer_string = "CC{[>][<]CC(C)[>][<]}CC(C)=C"
+polymer = bigsmiles.BigSMILES(polymer_string)
+text = bs_methods.print_tree(polymer)
+print(text)
+```
+
+#### Output:
+```text
+BigSMILES: CC{[>][<]CC(C)[>][<]}CC(C)=C
+├── Atom: C
+├── Bond: 
+├── Atom: C
+├── Bond: 
+├── StochasticObject: {[>][<]CC(C)[>][<]}
+│    └── StochasticFragment: [<]CC(C)[>]
+│        ├── BondDescriptorAtom: [<]
+│        ├── Bond: 
+│        ├── Atom: C
+│        ├── Bond: 
+│        ├── Atom: C
+│        ├── Branch: (C)
+│        │    ├── Bond: 
+│        │    └── Atom: C
+│        ├── Bond: 
+│        └── BondDescriptorAtom: [>]
+├── Bond: 
+├── Atom: C
+├── Bond: 
+├── Atom: C
+├── Branch: (C)
+│    ├── Bond: 
+│    └── Atom: C
+├── Bond: =
+└── Atom: C
+```
+
+
+## Configuration
+
+::: bigsmiles.methods.display_methods.tree_to_string.TreeConfig
\ No newline at end of file
diff --git a/docs/reference/overview.md b/docs/reference/overview.md
new file mode 100644
index 0000000..8eca0d5
--- /dev/null
+++ b/docs/reference/overview.md
@@ -0,0 +1,49 @@
+# Overview
+
+---
+
+** The code is organized in to 3 main groups: **
+
+!!! info "Data Structures"
+    Objects that hold data.
+
+    **<ins>Examples</ins>**
+
+    * Atom
+    * Bond
+    * Bonding Descriptor
+
+
+!!! info "Methods"
+    Functions that can transform, compute, display, etc. things about the data structure.
+
+    **<ins>Examples</ins>**
+
+    * Parse Tree
+    * Graph
+    * Fingerprint
+
+
+!!! info "Constructor"
+    Functions that can be used to construct data structures.
+
+    **<ins>Examples</ins>**
+
+    * add_atom()
+    * add_bond()
+    * insert_branch()
+
+
+---
+
+** The typical flow of data through the code is: **
+
+```mermaid
+flowchart LR
+  Constructor --> Data_Structures
+  Data_Structures  --> Methods
+
+```
+Note: the constructor is often called from the data_structure. Specifically, BigSMILES will pass the BigSMILES string 
+to the constructor to create itself.
+
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
new file mode 100644
index 0000000..e318727
--- /dev/null
+++ b/docs/stylesheets/extra.css
@@ -0,0 +1,50 @@
+
+[data-md-color-scheme="light"] {
+    --md-primary-fg-color: #a31f34;
+    --md-default-bg-color: #ffffff;
+    --md-default-fg-color--light: #000000;
+    --md-typeset-a-color: #852525;
+    --md-typeset-table-color: #000000;
+    --md-typeset-table-color--light: #de818f;
+    --md-accent-fg-color: #a31f34;
+    --md-code-hl-constant-color: #a31f34;
+}
+
+[data-md-color-scheme="dark"] {
+    --md-primary-fg-color: #a31f34;
+    --md-default-bg-color: #4b4f52;
+    --md-default-fg-color--light: #ffffff;
+    --md-accent-fg-color: #b48585;
+
+    --md-typeset-color: #ffffff;
+    --md-typeset-a-color: #d66f80;
+    --md-typeset-mark-color: #a31f34;
+    --md-typeset-table-color: #ffffff;
+    --md-typeset-table-color--light: #913141;
+
+    --md-code-bg-color: #676767;
+    --md-code-fg-color: #ffffff;
+    --md-code-hl-name-color: #ffffff;
+    --md-code-hl-operator-color: #ffffff;
+    --md-code-hl-punctuation-color: #ffffff;
+    --md-code-hl-constant-color: #93aaf3;
+    --md-code-hl-string-color: #73c261;
+    --md-code-hl-function-color: #ffffff;
+    --md-code-hl-comment-color: #919191;
+    --md-code-hl-variable-color: #ffffff;
+    --md-code-hl-keyword-color: #f29b46;
+
+    --md-code-hl-color: #21f511;
+    --md-code-hl-number-color: #21f511;
+    --md-code-hl-special-color: #21f511;
+    --md-code-hl-generic-color: #6796bb;
+
+    --md-admonition-fg-color: #ffffff;
+    --md-admonition-bg-color: #656262;
+    --md-text-link-color: #913141;
+}
+
+/* Maximum space for text block */
+.md-grid {
+    max-width: 90%;
+}
\ No newline at end of file
diff --git a/docs/stylesheets/mkdocstrings.css b/docs/stylesheets/mkdocstrings.css
new file mode 100644
index 0000000..e9e796d
--- /dev/null
+++ b/docs/stylesheets/mkdocstrings.css
@@ -0,0 +1,26 @@
+/* Indentation. */
+div.doc-contents:not(.first) {
+  padding-left: 25px;
+  border-left: .05rem solid var(--md-typeset-table-color);
+}
+
+/* Mark external links as such */
+a.autorefs-external::after {
+  /* https://primer.style/octicons/arrow-up-right-24 */
+  background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="rgb(0, 0, 0)" d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path></svg>');
+  content: ' ';
+
+  display: inline-block;
+  position: relative;
+  top: 0.1em;
+  margin-left: 0.2em;
+  margin-right: 0.1em;
+
+  height: 1em;
+  width: 1em;
+  border-radius: 100%;
+  background-color: var(--md-typeset-a-color);
+}
+a.autorefs-external:hover::after {
+  background-color: var(--md-accent-fg-color);
+}
diff --git a/docs/tutorials/tutorials.md b/docs/tutorials/tutorials.md
new file mode 100644
index 0000000..1fb73ed
--- /dev/null
+++ b/docs/tutorials/tutorials.md
@@ -0,0 +1,1 @@
+This part of the project documentation focuses on a **problem-oriented** approach or cookbook recipes. 
\ No newline at end of file
diff --git a/examples/_development.py b/examples/_development.py
index 2409299..183b25b 100644
--- a/examples/_development.py
+++ b/examples/_development.py
@@ -1,10 +1,10 @@
 
-import bigsmiles.tokenizer
+import bigsmiles.constructors.tokenizer
 
 
 def generate_list_of_tokens():
     text = "C%123"
-    results = bigsmiles.tokenizer.tokenize(text)
+    results = bigsmiles.constructors.tokenizer.tokenize(text)
 
     output = "[" + ", ".join([str(token.kind) for token in results]) + "]"
     print(output)
diff --git a/examples/example.py b/examples/example.py
index 95a8cdb..6833673 100644
--- a/examples/example.py
+++ b/examples/example.py
@@ -1,44 +1,78 @@
+import logging
+
 import bigsmiles
 
-import chemdraw
+# import chemdraw
 
 # bigsmiles.Config.color_output = True
 
 cases = [
-    "O1CCCCC1N1CCCCC1",
-    "F/C=C/F",
-    "C1=CC=CC=C1C",
-    "F{[$][$]CC(CC[$])[$][$]}O",
-    "OC{[>][<]C(=O)OCC(=O)[<],[>]NCCCCCC(C)N[>][<]}CF",
-    "CC{[<]CC(C[>])C(C[<])CO[>]}O",
-    "CC(F){[<1][>1]NN[<1],[>1]CC(C)[<2],[>2]OO[<2][>2]}CCO",
-    "F{[<][>]CC(F)[<],[>]CCO[<][>]}P",
-    "CC{[>][<]C(=O)CCCCC(=O)NCCCCCCN[>][<]}O",
-    "F{[<][>]OO[>],[>]C(N[<])C[<][>]}S",
+    "F/C(C)=C(C(OC(NBr)C)C(C)(F)C)\C(OC(N)C)C(F)=C",
+
+    "F/C=C/F",  # E-difluoroethene
+    "F\C=C\F",  # Z-difluoroethene
+    "C(\F)=C/F",  # trans
+    "F\C=C/F",  # cis
+    "F/C=C\F",  # cis
+    "F/C(CC)=C/F",
+    "F/C=C=C=C/F",  # trans
+    "F/C=C=C=C\F", # cis
+    "F/C=C/C/C=C\C",
+    "F/C=C/CC=CC",  # partially specified
+    "CC(F)/C=C/F",
+    "CCC(\F)=C/F",  #
+    "CCC(\F)=C(/F)CC",
+    "CC/C(\F)=C/F",  # error
+    "C(/[Ru])=C\C",
+    "CC/C(F)=N\F",
+
+    # "{[][<]OCCO[<],[>]C(=O)c1ccc(cc1)C(=O)[>],[>][H],[<]O[]}",
+    # "C(={[>][<]=CC=[>][<]}=1)CCCCCC1",
+    # "C=1CCC(={[>][<]=CC=[>][<]}=1)CCC",
+    # "C({[>][<]CCO[>][<]}1)CCCCCC1",
+    # "{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}",
+    # "CCC1(CO)C(O)CC(O1)N",
+    # "F/C=C/F",
+    # "C1=CC=CC=C1C",
+    # "F{[$][$]CC(CC[$])[$][$]}O",
+    # "OC{[>][<]C(=O)OCC(=O)[<],[>]NCCCCCC(C)N[>][<]}CF",
+    # "CC{[<]CC(C[>])C(C[<])CO[>]}O",
+    # "CC(F){[<1][>1]NN[<1],[>1]CC(C)[<2],[>2]OO[<2][>2]}CCO",
+    # "F{[<][>]CC(F)[<],[>]CCO[<][>]}P",
+    # "CC{[>][<]C(=O)CCCCC(=O)NCCCCCCN[>][<]}O",
+    # "F{[<][>]OO[>],[>]C(N[<])C[<][>]}S",
 ]
 
 
 def single():
+    logging.basicConfig(level=logging.DEBUG)
+
     case = cases[0]
     print("case: ", case)
     output = bigsmiles.BigSMILES(case)
     print("output: ", output)
-    output.print_tree(print_repr=False)
+    for i, bond in enumerate(output.bonds):
+        print(i, "   ", bond, bond.bond_order, bond.double_bond_ez)
+    # output.print_tree(print_repr=False)
 
-    drawer = chemdraw.Drawer(str(output))
-    drawer.draw_html(auto_open=True)
+    # drawer = chemdraw.Drawer(str(output))
+    # drawer.draw_html(auto_open=True)
 
 
 def multiple():
     results = []
-    for case in cases:
+    for i, case in enumerate(cases):
         output = bigsmiles.BigSMILES(case)
-        print(case, " --> ", output)
-
-    drawer = chemdraw.GridDrawer(results)
-    drawer.draw_html(auto_open=True)
+        print(i, "  ", case, " --> ", output)
+        results.append(str(output))
+        for bond in output.bonds:
+            print(bond, bond.bond_order, bond.double_bond_ez)
+    # drawer = chemdraw.GridDrawer(results)
+    # drawer.draw_html(auto_open=True)
 
 
 if __name__ == "__main__":
     single()
     # multiple()
+
+
diff --git a/examples/example_rxn.py b/examples/example_rxn.py
new file mode 100644
index 0000000..f4d64c1
--- /dev/null
+++ b/examples/example_rxn.py
@@ -0,0 +1,44 @@
+import bigsmiles
+
+import chemdraw
+
+
+cases = [
+    "C=Cc1ccccc1.C[CH-](.[Li+])CC>Cc1ccccc1>CC(CC){[>][<]CC(c1ccccc1)[>][<]}[H]",
+
+    "C=CCBr >> C=CCI",
+    "[I-].[Na+].C=CCBr>>[Na+].[Br-].C=CCI",
+    "C=CCBr.[Na+].[I-]>CC(=O)C>C=CCI.[Na+].[Br-]",
+    "CC(=O)[OH]>>CC(=O)OCC",
+    "CC(=O)[OH] >> CC(=O)OCC",  # extra spaces
+    "C=Cc1ccccc1.C[CH-](.[Li+])CC>Cc1ccccc1>CC(CC){[>][<]CC(c1ccccc1)[>][<]}[H]",
+    "C=C(C(=O)OC)>CCCCSC(=S)SC(C)C(=O)O,CS(=O)C>{[>][<]CC(C(=O)OC)[>][<]}",  # RAFT
+    "[CH2:1]=[CH:2][CH:3]=[CH:4][CH2:5][H:6]>> [H:6][CH2:1][CH:2]=[CH:3][CH:4]=[CH2:5]",
+    "CC(=O)O.OCC>[H+].[Cl-].OCC>CC(=O)OCC",
+]
+
+
+def single():
+    case = cases[0]
+    print("case: ", case)
+    output = bigsmiles.Reaction(case)
+    print("output: ", output)
+
+    # drawer = chemdraw.Drawer(str(output))
+    # drawer.draw_html(auto_open=True)
+
+
+def multiple():
+    results = []
+    for i, case in enumerate(cases):
+        output = bigsmiles.Reaction(case)
+        print(i, "  ", case, " --> ", output)
+        results.append(str(output))
+
+    # drawer = chemdraw.GridDrawer(results)
+    # drawer.draw_html(auto_open=True)
+
+
+if __name__ == "__main__":
+    single()
+    # multiple()
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..660b944
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,108 @@
+site_name: BigSMILES
+
+repo_url: https://github.com/dylanwal/BigSMILES
+repo_name: BigSMILES
+
+theme:
+  name: material
+  logo: img/poly_cloud.png
+  icon:
+    repo: fontawesome/brands/github
+  features:
+    - navigation.tabs
+    - navigation.tabs.sticky
+    - toc.integrate
+  palette:
+    - media: "(prefers-color-scheme: light)"
+      scheme: light
+      toggle:
+        icon: material/weather-sunny
+        name: Switch to dark mode
+    - media: "(prefers-color-scheme: dark)"
+      scheme: dark
+      toggle:
+        icon: material/weather-night
+        name: Switch to light mode
+
+plugins:
+  - search
+  - mermaid2
+  - mkdocstrings:
+      default_handler: python
+      handlers:
+        python:
+          paths: [src]
+          selection:
+            docstring_style: numpy
+          options:
+            heading_level: 3
+            show_root_heading: true
+            show_bases: false
+            show_source: false
+            docstring_section_style: spacy
+            merge_init_into_class: true
+
+nav:
+  - Home: index.md
+  - Quickstart:
+      - Overview: quickstart/overview.md
+      - Lesson 1: quickstart/quickstart.md
+  - Tutorials: tutorials/tutorials.md
+  - Reference:
+      - Overview: reference/overview.md
+      - Data Structures:
+          - Overview: reference/data_structure/overview.md
+          - BigSMILES: reference/data_structure/BigSMILES.md
+          - Atom: reference/data_structure/Atom.md
+          - Bond: reference/data_structure/Bond.md
+          - Branch: reference/data_structure/Branch.md
+          - Bond Descriptor: reference/data_structure/Bond_descriptor.md
+          - Bond Descriptor Atom: reference/data_structure/Bond_descriptor_atom.md
+          - Stochastic Fragment: reference/data_structure/Stochastic_fragment.md
+          - Stochastic Object: reference/data_structure/Stochastic_object.md
+          - Reaction: reference/data_structure/Reaction.md
+      - Methods:
+          - Overview: reference/methods/overview.md
+          - parse_tree: reference/methods/parse_tree.md
+      - Constructor:
+          - Overview: reference/constructor/overview.md
+          - Constructor: reference/constructor/constructor.md
+          - Constructor(strings): reference/constructor/constructor_str.md
+          - Constructor(tokens): reference/constructor/constructor_tokens.md
+          - Constructor(reaction): reference/constructor/constructor_reaction.md
+          - Tokenizer: reference/constructor/tokenizer.md
+      - Configuration: reference/configuration.md
+      - Errors: reference/errors.md
+  - BigSMILES:
+      - Overview: bigsmiles/overview.md
+      - Specification: bigsmiles/specification.md
+      - Examples: bigsmiles/examples.md
+      - References: bigsmiles/references.md
+  - Developers: developers/developers.md
+  - Extras:
+      - Change Log: extras/change_log.md
+      - License: extras/license.md
+
+markdown_extensions:
+  - toc:
+      baselevel: 1
+      toc_depth: 3
+      permalink: True
+  - admonition
+  - pymdownx.details
+  - pymdownx.highlight:
+      anchor_linenums: true
+  - pymdownx.inlinehilite
+  - pymdownx.snippets
+  - pymdownx.caret
+  - pymdownx.tilde
+  - pymdownx.superfences:
+      # make exceptions to highlighting of code:
+      custom_fences:
+        - name: mermaid
+          class: mermaid
+          format: !!python/name:mermaid2.fence_mermaid
+
+extra_css:
+  - stylesheets/extra.css
+  - stylesheets/mkdocstrings.css
diff --git a/requirements_dev.txt b/requirements_dev.txt
index c202ca1..3dbe035 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -2,3 +2,8 @@ pytest==7.2.0
 pytest-cov==2.12.1
 twine==4.0.2
 build==0.9.0
+mkdocs==1.4.2
+mkdocs-material==9.1.1
+mkdocstrings[python]==0.20.0
+pymdown-extensions==9.10
+mkdocs-mermaid2-plugin==0.6.0
diff --git a/setup.cfg b/setup.cfg
index f09556d..237390c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = bigsmiles
-version = 0.0.4
+version = 0.0.7
 description = BigSMILES Parser
 long_description = file: README.md
 long_description_content_type = text/markdown
@@ -13,13 +13,13 @@ classifiers =
     License :: OSI Approved :: BSD License
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3 :: Only
-    Programming Language :: Python :: 3.10
+    Programming Language :: Python :: 3.7
 
 [options]
 packages =
     bigsmiles
 package_dir =
     =src
-python_requires = >=3.10
+python_requires = >=3.7
 zip_safe = no
 include_package_data = true
diff --git a/src/bigsmiles/README.md b/src/bigsmiles/README.md
index 23d2824..121c74a 100644
--- a/src/bigsmiles/README.md
+++ b/src/bigsmiles/README.md
@@ -37,3 +37,11 @@ Order of creating objects:
 2) Add bond and atom together
    1) If bond not present (i.e. it is a single bond) add it manually. 
 
+# TODO
+* add cis/trans
+* aromatic stuff
+* graph representation
+* adacency matrix
+* chemical formula
+* fingerprints
+* documentation
\ No newline at end of file
diff --git a/src/bigsmiles/__init__.py b/src/bigsmiles/__init__.py
index 8d5dcb0..2fc0203 100644
--- a/src/bigsmiles/__init__.py
+++ b/src/bigsmiles/__init__.py
@@ -1,14 +1,8 @@
 import pkg_resources
 __version__ = pkg_resources.require("bigsmiles")[0].version
 
-
 from bigsmiles.config import Config
-from bigsmiles.errors import BigSMILESError
-from bigsmiles.tokenizer import BigSMILESTokenizeError
-from bigsmiles.bigsmiles import Atom, Bond, Branch, BondDescriptor, StochasticObject, StochasticFragment, BigSMILES
-
-errors = [
-    BigSMILESError,
-    BigSMILESTokenizeError
-]
-
+from bigsmiles.errors import ERRORS
+from bigsmiles.data_structures.bigsmiles import Atom, Bond, Branch, BondDescriptor, BondDescriptorAtom, \
+    StochasticObject, StochasticFragment, BigSMILES
+from bigsmiles.data_structures.reaction import Reaction
diff --git a/src/bigsmiles/bigsmiles.py b/src/bigsmiles/bigsmiles.py
deleted file mode 100644
index 43b804b..0000000
--- a/src/bigsmiles/bigsmiles.py
+++ /dev/null
@@ -1,405 +0,0 @@
-from __future__ import annotations
-
-from bigsmiles.config import Config
-
-
-class Atom:
-    __slots__ = ["id_", "element", "isotope", "stereo", "hydrogens", "charge", "valence",
-                 "organic", "bonds", "possible_valence", "_default_valance", "__dict__"]
-    _tree_print_repr = True
-
-    def __init__(self,
-                 id_: int,
-                 element: str,
-                 isotope: int | None = None,
-                 stereo: str = '',
-                 hydrogens: int = 0,
-                 charge: int = 0,
-                 valence: int = None,
-                 **kwargs
-                 ):
-        self.id_ = id_
-        self.element = element
-        self.isotope = isotope
-        self.stereo = stereo
-        self.hydrogens = hydrogens
-        self.charge = charge
-        self.possible_valence: tuple[int] = Config.get_atom_possible_valence(element)
-        if valence is None:
-            self._default_valance: bool = True
-            self.valence = self.possible_valence[0]
-        else:
-            self._default_valance: bool = False
-            self.valence = valence
-        self.organic = True if element in Config.organics else False
-
-        if kwargs:
-            for k, v in kwargs.items():
-                setattr(self, k, v)
-
-        self.bonds = []
-
-    def __str__(self):
-        return self.to_string()
-
-    def __repr__(self):
-        return str(self) + "{" + str(self.id_) + "}"
-
-    @property
-    def bond_capacity(self) -> int:
-        return self.valence - self.hydrogens + self.charge
-
-    @property
-    def number_of_bonds(self) -> int:
-        return sum((bond.bond_order for bond in self.bonds)) + self.hydrogens
-
-    @property
-    def bonds_available(self) -> int:
-        return self.bond_capacity - self.number_of_bonds
-
-    @property
-    def ring_indexes(self) -> list[int]:
-        ring_index = []
-        for bond in self.bonds:
-            if bond.ring_id is not None:
-                ring_index.append(bond.ring_id)
-
-        return ring_index
-
-    def to_string(self, show_hydrogens: bool = False, print_repr: bool = False, skip_color: bool = False) -> str:
-        text = self.element
-        bracket_flag = False
-
-        if self.element == "H":
-            bracket_flag = True
-
-        if self.stereo:
-            text += self.stereo
-            bracket_flag = True
-
-        if show_hydrogens or self.hydrogens > 0:
-            if self.hydrogens > 0 or self.bonds_available != 0:
-                text += f"H{self.bonds_available + self.hydrogens}"
-                bracket_flag = True
-
-        if self.charge > 0:
-            text += f"+{self.charge if self.charge > 1 else ''}"
-            bracket_flag = True
-        elif self.charge < 0:
-            text += f"{self.charge}"
-            bracket_flag = True
-
-        if self.isotope is not None:
-            text = str(self.isotope) + text
-            bracket_flag = True
-
-        if bracket_flag:
-            text = "[" + text + "]"
-
-        if self.ring_indexes:
-            for bond in self.bonds:
-                if bond.ring_id is not None:
-                    if bond.ring_id > 9:
-                        ring_id = "%" + str(bond.ring_id)
-                    else:
-                        ring_id = str(bond.ring_id)
-                    text += bond.symbol + ring_id
-
-        return text
-
-    def _increase_valance(self, requested_valance_increase: int = 0) -> bool:
-        " If default valance, try to increase when an additional bond added that needs it. "
-        if not self._default_valance:
-            return False
-
-        for valence in self.possible_valence:
-            if valence > self.valence:
-                old_valance = self.valence
-                self.valence = valence
-                if self.bonds_available < requested_valance_increase:
-                    # increase was not enough for requested valance increase; set valence back to original value
-                    self.valence = old_valance
-                    continue
-
-                return True
-
-        return False
-
-
-bond_mapping = {
-    "": 1,
-    "=": 2,
-    "#": 3
-}
-
-
-class Bond:
-    __slots__ = ["id_", "symbol", "atom1", "atom2", "ring_id", "__dict__"]
-    _tree_print_repr = True
-
-    def __init__(self,
-                 symbol: str,
-                 atom1: Atom | BondDescriptorAtom | StochasticObject,
-                 atom2: Atom | BondDescriptorAtom | StochasticObject | None = None,
-                 id_: int = None,
-                 ring_id: int = None
-                 ):
-        self.id_ = id_
-        self.symbol = symbol
-        self.atom1 = atom1
-        self.atom2 = atom2
-        self.ring_id = ring_id
-
-    def __str__(self):
-        return self.to_string()
-
-    def __repr__(self):
-        return self.to_string(print_repr=True, skip_color=True)
-
-    def to_string(self, show_hydrogens: bool = False, print_repr: bool = False, skip_color: bool = False):
-        return Config.add_color(self.symbol, 'Blue', skip_color)
-
-    def __iter__(self):
-        return iter((self.atom1, self.atom2))
-
-    @property
-    def bond_order(self) -> int:
-        return bond_mapping[self.symbol]
-
-    @bond_order.setter
-    def bond_order(self, count: int):
-        for k, v in bond_mapping.items():
-            if v == count:
-                self.symbol = k
-                return
-
-        raise ValueError(f"Invalid bond_order. \nGiven: {count} \n Acceptable: {bond_mapping}")
-
-
-class BondDescriptor:
-    __slots__ = ["descriptor", "index_", "instances", "stochastic_object", "__dict__"]
-
-    def __init__(self, stochastic_object: StochasticObject, descriptor: str, index_: int):
-        self.descriptor = descriptor
-        self.index_ = index_
-        self.instances = []
-        self.stochastic_object = stochastic_object
-
-    def __str__(self):
-        return self.to_string()
-
-    def __repr__(self):
-        return self.to_string(print_repr=True, skip_color=True)
-
-    def to_string(self, show_hydrogens: bool = False, print_repr: bool = False, skip_color: bool = False):
-        if self.index_ == 1 and not Config.show_bond_descriptor_zero_index:
-            index = ""
-        else:
-            index = self.index_
-        return "[" + self.descriptor + str(index) + "]"
-
-    @property
-    def symbol(self) -> str:
-        return self.descriptor + str(self.index_)
-
-    def is_pair(self, bd: BondDescriptor) -> bool:
-        """ Returns true if symbols are <> and index match. """
-        if self.index_ == bd.index_:
-            if (self.descriptor == "<" and bd.descriptor == ">") or \
-                    (self.descriptor == ">" and bd.descriptor == "<"):
-                return True
-
-        return False
-
-
-class BondDescriptorAtom:
-    _tree_print_repr = True
-    __slots__ = ["descriptor", "id_", "bond", "__dict__"]
-
-    def __init__(self, bond_descriptor: BondDescriptor, id_: int = None):
-        self.descriptor = bond_descriptor
-        bond_descriptor.instances.append(self)
-        self.id_ = id_
-        self.bond = None
-
-    def __str__(self):
-        return self.to_string()
-
-    def __repr__(self):
-        return self.to_string(print_repr=True, skip_color=True)
-
-    def to_string(self, show_hydrogens: bool = False, print_repr: bool = False, skip_color: bool = False):
-        return Config.add_color(self.descriptor.to_string(show_hydrogens, print_repr), 'Green', skip_color)
-
-
-class Branch:
-    _tree_print_repr = False
-    __slots__ = ["nodes", "id_", "parent", "__dict__"]
-
-    def __init__(self, parent: BigSMILES | StochasticFragment | Branch, id_: int = None):
-        self.nodes: list[Atom | Bond | Branch | StochasticObject | BondDescriptorAtom] = []
-        self.id_ = id_
-        self.parent = parent
-
-    def __str__(self):
-        return self.to_string()
-
-    def __repr__(self):
-        return self.to_string(print_repr=True, skip_color=True)
-
-    def to_string(self, show_hydrogens: bool = False, print_repr: bool = False, skip_color: bool = False):
-        text = "".join(
-            node.to_string(show_hydrogens, print_repr) for node in self.nodes
-        )
-
-        return "(" + text + ")"
-
-    @property
-    def in_stochastic_object(self) -> bool:
-        return self.parent.in_stochastic_object
-
-    @property
-    def root(self) -> BigSMILES:
-        return self.parent.root
-
-
-class StochasticFragment:
-    _tree_print_repr = False
-    __slots__ = ["nodes", "id_", "parent", "bonding_descriptors", "rings", "__dict__"]
-
-    def __init__(self, parent: StochasticObject, id_: int = None):
-        self.nodes: list[Atom | Bond | BondDescriptorAtom | Branch | StochasticObject] = []
-        self.rings: list[Bond] = []
-        self.bonding_descriptors: list[BondDescriptor] = []
-        self.id_ = id_
-        self.parent = parent
-
-    def __str__(self):
-        return self.to_string()
-
-    def __repr__(self):
-        return self.to_string(print_repr=True, skip_color=True)
-
-    def to_string(self, show_hydrogens: bool = False, print_repr: bool = False, skip_color: bool = False):
-        return "".join(node.to_string(show_hydrogens, print_repr, skip_color) for node in self.nodes)
-
-    @property
-    def in_stochastic_object(self) -> bool:
-        return self.parent.in_stochastic_object
-
-    @property
-    def root(self) -> BigSMILES:
-        return self.parent.root
-
-
-class StochasticObject:
-    _tree_print_repr = False
-    __slots__ = ["nodes", "bonding_descriptors", "id_", "parent", "end_group_left", "end_group_right",
-                 "bond_left", "bond_right", "__dict__"]
-
-    def __init__(self, parent: BigSMILES | StochasticFragment | Branch, id_: int = None):
-        self.nodes: list[StochasticFragment] = []
-        self.bonding_descriptors: list[BondDescriptor] = []
-        self.end_group_left: BondDescriptorAtom | None = None
-        self.end_group_right: BondDescriptorAtom | None = None
-        self.id_ = id_
-        self.bond_left: Bond | None = None
-        self.bond_right: Bond | None = None
-        self.parent = parent
-
-    def __str__(self):
-        return self.to_string()
-
-    def __repr__(self):
-        return self.to_string(print_repr=True, skip_color=True)
-
-    def to_string(self, show_hydrogens: bool = False, print_repr: bool = False, skip_color: bool = False):
-        text = Config.add_color("{", "Red", skip_color)
-        text += self.end_group_left.to_string(show_hydrogens, print_repr, skip_color)
-        text += ",".join(node.to_string(show_hydrogens, print_repr, skip_color) for node in self.nodes)
-        text += self.end_group_right.to_string(show_hydrogens, print_repr, skip_color)
-        return text + Config.add_color("}", "Red", skip_color)
-
-    @property
-    def implicit_endgroups(self) -> bool:
-        """ Returns true if one or more are implicit """
-        return True if self.end_group_left.descriptor.descriptor == "" or \
-                       self.end_group_right.descriptor.descriptor == "" else False
-
-    @property
-    def in_stochastic_object(self) -> bool:
-        return True
-
-    @property
-    def root(self) -> BigSMILES:
-        return self.parent.root
-
-
-def contains_stochastic_object(nodes: Atom | Bond | StochasticObject | Branch):
-    for node in nodes:
-        if isinstance(node, StochasticObject):
-            return True
-        if isinstance(node, Branch):
-            result = contains_stochastic_object(node.nodes)
-            if result:
-                return True
-
-    return False
-
-
-class BigSMILES:
-    _tree_print_repr = False
-    __slots__ = ["nodes", "atoms", "bonds", "rings", "__dict__"]
-
-    def __init__(self, input_text: str = None):
-        self.nodes: list[Atom | Bond | StochasticObject | Branch] = []
-        self.atoms: list[Atom] = []  # includes atoms in sub-objects
-        self.bonds: list[Bond] = []  # includes bonds in sub-objects
-        self.rings: list[Bond] = []  # does not include rings in sub-objects
-
-        # parse input string
-        if input_text:
-            from bigsmiles.parse_bigsmiles_str import parse_bigsmiles_str
-            parse_bigsmiles_str(input_text, self)
-
-    def __str__(self):
-        return self.to_string()
-
-    def __repr__(self):
-        return self.to_string(print_repr=True, skip_color=True)
-
-    def __getitem__(self, obj: int | slice) -> Atom | list[Atom]:
-        return self.atoms[obj]
-
-    def __iter__(self) -> list[Atom]:
-        return self.atoms
-
-    def to_string(self, show_hydrogens: bool = False, print_repr: bool = False, skip_color: bool = False) -> str:
-        return "".join(node.to_string(show_hydrogens, print_repr, skip_color) for node in self.nodes)
-
-    @property
-    def in_stochastic_object(self) -> bool:
-        return False
-
-    @property
-    def contains_stochastic_object(self) -> bool:
-        return contains_stochastic_object(self.nodes)
-
-    @property
-    def root(self) -> BigSMILES:
-        return self
-
-    def print_tree(self, show_object_label: bool = True, print_repr: bool = False):
-        """
-        prints a tree representation of the parsed bigsmiles
-
-        Parameters
-        ----------
-        show_object_label: bool
-            show object labels
-        print_repr: bool
-            use repr() instead of str()
-
-        """
-        from bigsmiles.tree_to_string import tree_to_string  # here to avoid circular imports
-        print(tree_to_string(self, show_object_label, print_repr))
diff --git a/src/bigsmiles/config.py b/src/bigsmiles/config.py
index 180cb65..9d27417 100644
--- a/src/bigsmiles/config.py
+++ b/src/bigsmiles/config.py
@@ -17,94 +17,17 @@ class TerminalColors:
 
 
 class Config:
+    # Will show colors in terminal when any object is printed
     color_output = False
-    show_bond_descriptor_zero_index = False
 
-    elements = {
-        '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', 'D'
-    }
+    # True: [$1]; False: [$]
+    show_bond_descriptor_one_index = False
 
-    elements_ordered = sorted(elements, reverse=True)  # sorted in reverse so "Cl" hits before "C"
+    # True: will add ':' symbol for aromatic_elements bonds, False: hides the ':' in string outputs
+    show_aromatic_bond = True
 
-    aromatic = {
-        "b",
-        "c",
-        "n",
-        "o",
-        "p",
-        "s"
-    }
-
-    organics = {
-        "B",
-        "C",
-        "N",
-        "O",
-        "P",
-        "S",
-        "F",
-        "Cl",
-        "Br",
-        "I"
-    }
-
-    atom_valences = {
-        # put smallest valance first in tuple
-        "H": (1,),
-        "D": (1,),
-        "Li": (1,),
-        "Be": (2,),
-        "B": (3,),
-        "C": (4,),
-        "N": (3,),
-        "O": (2,),
-        "F": (1,),
-        "Na": (1,),
-        "Mg": (2,),
-        "Si": (4,),
-        "P": (3, 5),
-        "P5": (5,),  #
-        "S": (2, 4, 6),
-        "S4": (4,),  #
-        "S6": (6,),  #
-        "Cl": (1, 3, 5, 7),
-        "K": (1,),
-        "Ca": (2,),
-        "Ga": (3,),
-        "Ge": (4,),
-        "As": (3, 5),
-        "Se": (2, 4, 6),
-        "Br": (1,),
-        "Rb": (1,),
-        "Sr": (2,),
-        "In": (3,),
-        "Sn": (2, 4),
-        "Sb": (3, 5),
-        "Te": (2, 4, 6),
-        "I": (1, 3, 5, 7),
-        "Cs": (1,),
-        "Ba": (2,),
-        "Ti": (3,),
-        "Pb": (2, 4),
-        "Bi": (3, 5),
-        "Po": (2, 4, 6),
-        "At": (1, 3, 5, 7),
-        "Fr": (1,),
-        "Ra": (2,),
-    }
-
-    @staticmethod
-    def get_atom_possible_valence(symbol: str) -> tuple:
-        try:
-            return Config.atom_valences[symbol]
-        except KeyError:
-            return (8,)
+    # for small molecules only. True: 'C=1CCCCC=1'  False: 'C=1CCCCC1'
+    show_multi_bonds_on_both_ring_index = False
 
     @classmethod
     def add_color(cls, text: str, color: str, skip_color: bool = False) -> str:
diff --git a/src/bigsmiles/constructor.py b/src/bigsmiles/constructor.py
deleted file mode 100644
index ada1f98..0000000
--- a/src/bigsmiles/constructor.py
+++ /dev/null
@@ -1,365 +0,0 @@
-import enum
-
-from bigsmiles.errors import BigSMILESError
-from bigsmiles.bigsmiles import Atom, Bond, BondDescriptor, Branch, StochasticFragment, StochasticObject, \
-    BigSMILES, BondDescriptorAtom
-from bigsmiles.validation import post_validation
-from bigsmiles.syntax_fixes import run_syntax_fixes
-
-
-class States(enum.Enum):
-    start = 0
-    atom = 1
-    bond_descriptor = 2
-    branch_start = 3
-    branch = 4
-    ring = 5
-    stochastic_fragment = 6
-    stochastic_object_start = 7
-    stochastic_object_end = 8
-    stochastic_object = 9
-
-
-def add_bond_to_connected_objects(bond: Bond):
-    """ Adds bonds to Atoms, and BondDescriptorAtom. """
-    for obj in bond:
-        if isinstance(obj, Atom):
-            if obj.bonds_available < bond.bond_order:
-                if obj._increase_valance(bond.bond_order):
-                    continue
-                raise BigSMILESError("Too many bonds trying to be made.", str(obj))
-            obj.bonds.append(bond)
-        elif isinstance(obj, BondDescriptorAtom):
-            obj.bond = bond
-        elif isinstance(obj, StochasticObject):  # left bond add with
-            obj.bond_right = bond
-
-
-def add_bond_descriptor_to_stochastic_fragment(stoch_frag: StochasticFragment, loop_obj: Branch = None):
-    if loop_obj is None:
-        loop_obj = stoch_frag
-
-    for obj in loop_obj.nodes:
-        if isinstance(obj, BondDescriptorAtom) and obj.descriptor not in stoch_frag.bonding_descriptors:
-            stoch_frag.bonding_descriptors.append(obj.descriptor)
-        if isinstance(obj, Branch):
-            add_bond_descriptor_to_stochastic_fragment(stoch_frag, obj)  # recursive
-
-
-def get_common_bond(atom1: Atom, atom2: Atom) -> Bond | None:
-    # Check if bond already between two atoms
-    for bond in atom1.bonds:
-        if bond in atom2.bonds:
-            return bond
-
-    return None  # No common bond found
-
-
-def remove_partial_ring(parent: BigSMILES | Branch | StochasticFragment, ring_id: int):
-    for i, ring in enumerate(parent.rings):
-        if ring.ring_id == ring_id:
-            break
-    else:
-        return
-
-    bond = parent.rings.pop(i)
-    parent.bonds.remove(bond)
-    if hasattr(parent, "parent"):
-        parent.parent.bonds.remove(bond)
-
-
-class BigSMILESConstructor:
-
-    def __init__(self, bigsmiles: BigSMILES = None, syntax_fix: bool = True):
-        self.bigsmiles = bigsmiles if bigsmiles is not None else BigSMILES()
-
-        self.syntax_fix = syntax_fix  # only with context manager
-
-        # setup id counters
-        self._atom_counter = 0
-        self._bond_counter = 0
-        self._bond_descriptor_atom = 0
-        self._branch_counter = 0
-        self._stochastic_fragment = 0
-        self._stochastic_object = 0
-        self._ring_counter = 1  # start ring counter at 1
-
-        self.state = States.start
-        self.stack: list[BigSMILES | StochasticObject | StochasticFragment | Branch] = [self.bigsmiles]
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_value, exc_tb):
-        if exc_type:
-            raise exc_type(exc_value).with_traceback(exc_tb)
-
-        if len(self.stack) != 1:
-            raise BigSMILESError(f"{type(self.stack[-1])} is missing closing symbol.")
-        if self.syntax_fix:
-            run_syntax_fixes(self.bigsmiles)
-
-    def _get_atom_id(self) -> int:
-        self._atom_counter += 1
-        return self._atom_counter - 1
-
-    def _get_bond_id(self) -> int:
-        self._bond_counter += 1
-        return self._bond_counter - 1
-
-    def _get_ring_id(self) -> int:
-        self._ring_counter += 1
-        return self._ring_counter - 1
-
-    def _get_bond_descriptor_atom_id(self) -> int:
-        self._bond_descriptor_atom += 1
-        return self._bond_descriptor_atom - 1
-
-    def _get_branch_id(self) -> int:
-        self._branch_counter += 1
-        return self._branch_counter - 1
-
-    def _get_stochastic_fragment_id(self) -> int:
-        self._stochastic_object += 1
-        return self._stochastic_object - 1
-
-    def _get_stochastic_object_id(self) -> int:
-        self._stochastic_object += 1
-        return self._stochastic_object - 1
-
-    def add_ring(self, ring_id: int, bond_symbol: str = "") -> Bond:
-        # check if ring_id already exists
-        ring_parent = self._get_ring_parent()
-        for ring in ring_parent.rings:
-            if ring.ring_id == ring_id:
-                if ring.atom2 is not None:
-                    raise BigSMILESError(f"Ring already formed for ring id {ring_id}.")
-                atom2 = self._get_prior(self.stack[-1], (Atom,))
-
-                bond = get_common_bond(ring.atom1, atom2)
-                if bond:  # Check if ring already between two atoms and just increase bond order
-                    bond.bond_order += 1
-                    remove_partial_ring(ring_parent, ring_id)
-                else:
-                    ring.atom2 = atom2
-                    add_bond_to_connected_objects(ring)
-                return ring
-
-        # Make new ring_id
-        bond = Bond(bond_symbol, self._get_prior(self.stack[-1], (Atom,)), None, self._get_bond_id(), ring_id)
-        self.bigsmiles.bonds.append(bond)
-        ring_parent.rings.append(bond)
-
-        return bond
-
-    def add_ring_from_atoms(self, atom1: Atom, atom2: Atom, bond_symbol: str = ""):
-        ring_parent = self._get_ring_parent()
-
-        bond = get_common_bond(atom1, atom2)
-        if bond:  # Check if ring already between two atoms and just increase bond order
-            bond.bond_order += 1
-        else:
-            # Make new ring_id
-            bond = Bond(bond_symbol, atom1, atom2, self._get_bond_id(), self._get_ring_id())
-            self.bigsmiles.bonds.append(bond)
-            ring_parent.rings.append(bond)
-            add_bond_to_connected_objects(bond)
-
-    def _get_ring_parent(self) -> BigSMILES | StochasticFragment:
-        for node in reversed(self.stack):
-            if hasattr(node, "rings"):
-                return node
-
-    def add_atom(self,
-                 element: str,
-                 isotope: int | None = None,
-                 stereo: str = '',
-                 hcount: int = 0,
-                 charge: int = 0,
-                 valance: int = None,
-                 **kwargs
-                 ) -> Atom:
-        atom = Atom(self._get_atom_id(), element, isotope, stereo, hcount, charge, valance, **kwargs)
-        self.stack[-1].nodes.append(atom)
-        self.bigsmiles.atoms.append(atom)
-
-        self.state = States.atom
-        return atom
-
-    def add_bond(self,
-                 bond_symbol: str,
-                 atom1: Atom | BondDescriptorAtom,
-                 atom2: Atom | BondDescriptorAtom | StochasticObject | None
-                 ) -> Bond:
-        bond = Bond(bond_symbol, atom1, atom2, self._get_bond_id())
-        self.stack[-1].nodes.append(bond)
-        self.bigsmiles.bonds.append(bond)
-        add_bond_to_connected_objects(bond)
-        return bond
-
-    def _get_bonding_descriptor_atom(self, descriptor: str, index_: int) -> BondDescriptorAtom:
-        bd = self._get_bonding_descriptor(descriptor, index_)
-        return BondDescriptorAtom(bd, self._get_bond_descriptor_atom_id())
-
-    def _get_bonding_descriptor(self, descriptor: str, index_: int) -> BondDescriptor:
-        stoch_obj = self._get_current_stochastic_object()
-
-        # check if bd in there already
-        for bd in stoch_obj.bonding_descriptors:
-            if descriptor == bd.descriptor and index_ == bd.index_:
-                return bd
-
-        # create new bonding descriptor
-        new_bd = BondDescriptor(stoch_obj, descriptor, index_)
-        stoch_obj.bonding_descriptors.append(new_bd)
-        return new_bd
-
-    def _get_current_stochastic_object(self) -> StochasticObject:
-        for obj in self.stack:
-            if isinstance(obj, StochasticObject):
-                return obj
-
-        raise BigSMILESError("Coding error.")
-
-    def add_bonding_descriptor(self, descriptor: str, index_: int) -> BondDescriptorAtom:
-        """ [<], [>], [$], [$1], [>2], ... """
-        bd_atom = self._get_bonding_descriptor_atom(descriptor, index_)
-        self.stack[-1].nodes.append(bd_atom)
-
-        self.state = States.bond_descriptor
-        return bd_atom
-
-    def add_bond_atom_pair(self,
-                           bond_symbol: str,
-                           element: str,
-                           isotope: int | None = None,
-                           stereo: str = '',
-                           hcount: int = 0,
-                           charge: int = 0,
-                           valance: int = None,
-                           **kwargs
-                           ) -> tuple[Bond, Atom]:
-        atom = Atom(self._get_atom_id(), element, isotope, stereo, hcount, charge, valance, **kwargs)
-        prior_atom = self._get_prior(self.stack[-1], (Atom, BondDescriptorAtom, StochasticObject))
-        bond = self.add_bond(bond_symbol, prior_atom, atom)
-        self.stack[-1].nodes.append(atom)
-        self.bigsmiles.atoms.append(atom)
-
-        self.state = States.atom
-        return bond, atom
-
-    def add_bond_bonding_descriptor_pair(self, bond_symbol: str, descriptor: str, index_: int) \
-            -> tuple[Bond, BondDescriptorAtom]:
-        bd_atom = self._get_bonding_descriptor_atom(descriptor, index_)
-        prior_atom = self._get_prior(self.stack[-1], (Atom, BondDescriptorAtom, StochasticObject))
-        bond = self.add_bond(bond_symbol, prior_atom, bd_atom)
-        self.stack[-1].nodes.append(bd_atom)
-
-        self.state = States.bond_descriptor
-        return bond, bd_atom
-
-    def open_branch(self):
-        if isinstance(self.stack[-1], Branch) and len(self.stack[-1].nodes) == 0:
-            raise BigSMILESError("BigSMILES string or branch can't start with branch")
-
-        branch = Branch(self.stack[-1], self._get_branch_id())
-        self.stack[-1].nodes.append(branch)
-        self.stack.append(branch)
-        self.state = States.branch_start
-
-    def close_branch(self):
-        if not isinstance(self.stack[-1], Branch):
-            raise BigSMILESError("Error closing branch. Possible issues: "
-                                 "\n\tClosing a branch with another intermediate node started."
-                                 "\n\tNo starting branch symbol.")
-
-        branch = self.stack.pop()
-
-        # check if branch is empty; if so remove it
-        if len(branch.nodes) == 0:
-            self.stack[-1].nodes.pop()
-            self._branch_counter -= 1
-
-    def open_stochastic_object(self, descriptor: str, index_: int) -> StochasticFragment:
-        stoch_obj = StochasticObject(self.stack[-1], self._get_stochastic_object_id())
-        new_bd = BondDescriptor(stoch_obj, descriptor, index_)
-        stoch_obj.bonding_descriptors.append(new_bd)
-        stoch_obj.end_group_left = BondDescriptorAtom(new_bd, self._get_bond_descriptor_atom_id())
-        self.stack[-1].nodes.append(stoch_obj)
-        self.stack.append(stoch_obj)
-
-        # open stochastic_fragment
-        stoch_frag = StochasticFragment(self.stack[-1], self._get_stochastic_fragment_id())
-        self.stack[-1].nodes.append(stoch_frag)
-        self.stack.append(stoch_frag)
-        self.state = States.stochastic_fragment
-        return stoch_frag
-
-    def open_stochastic_object_with_bond(self, bond_symbol: str, descriptor: str, index_: int) -> StochasticFragment:
-        stoch_obj = StochasticObject(self.stack[-1], self._get_stochastic_object_id())
-        new_bd = BondDescriptor(stoch_obj, descriptor, index_)
-        stoch_obj.bonding_descriptors.append(new_bd)
-        stoch_obj.end_group_left = BondDescriptorAtom(new_bd, self._get_bond_descriptor_atom_id())
-
-        prior_atom = self._get_prior(self.stack[-1], (Atom, BondDescriptor, StochasticObject))
-        bond = self.add_bond(bond_symbol, prior_atom, stoch_obj)
-        stoch_obj.bond_left = bond
-
-        self.stack[-1].nodes.append(stoch_obj)
-        self.stack.append(stoch_obj)
-
-        return self.open_stochastic_fragment()
-
-    def close_stochastic_object(self, descriptor: str, index_: int):
-        if not isinstance(self.stack[-1], StochasticObject):
-            raise BigSMILESError("Error closing StochasticObject. Possible issues: "
-                                 "\n\tClosing a StochasticObject with another intermediate node started."
-                                 "\n\tNo starting StochasticObject symbol.")
-
-        self.stack[-1].end_group_right = self._get_bonding_descriptor_atom(descriptor, index_)
-        self.stack.pop()
-
-    def open_stochastic_fragment(self) -> StochasticFragment:
-        stoch_frag = StochasticFragment(self.stack[-1], self._get_stochastic_fragment_id())
-        self.stack[-1].nodes.append(stoch_frag)
-        self.stack.append(stoch_frag)
-        self.state = States.stochastic_fragment
-        return stoch_frag
-
-    def close_open_stochastic_fragment(self) -> StochasticFragment:
-        if not isinstance(self.stack[-1], StochasticFragment):
-            raise BigSMILESError("Stochastic seperator can only follow a stochastic fragments.")
-
-        self.close_stochastic_fragment()
-        return self.open_stochastic_fragment()
-
-    def close_stochastic_fragment(self):
-        if not isinstance(self.stack[-1], StochasticFragment):
-            raise BigSMILESError("Error StochasticFragment branch. Possible issues: "
-                                 "\n\tClosing a branch with another intermediate node started.")
-
-        add_bond_descriptor_to_stochastic_fragment(self.stack[-1])
-
-        # check for at-least one bonding descriptor
-        if not self.stack[-1].bonding_descriptors:
-            raise BigSMILESError(f"No bonding descriptor in the stochastic fragment. ({repr(self.stack[-1])})")
-
-        self.stack.pop()
-
-    def _get_prior(self, obj: BigSMILES | StochasticObject | StochasticFragment | Branch, types_: tuple,
-                   flag: bool = False) -> Atom:
-        if obj.nodes:
-            for node in reversed(obj.nodes):  # no atom can ever have more than 10 bonds
-                if isinstance(node, types_):
-                    return node
-
-            raise BigSMILESError("Bug in the code")
-
-        elif not flag:
-            return self._get_prior(self.stack[-2], types_, flag=True)
-
-        raise BigSMILESError("Bond attempted to be made to that has nothing to bond back to.")
-
-    def run_validation(self):
-        """ Run various validations. """
-        post_validation(self.bigsmiles)
diff --git a/src/bigsmiles/constructor_str.py b/src/bigsmiles/constructor_str.py
deleted file mode 100644
index 2a9f09d..0000000
--- a/src/bigsmiles/constructor_str.py
+++ /dev/null
@@ -1,84 +0,0 @@
-import re
-from functools import wraps
-
-from bigsmiles.config import Config
-from bigsmiles.constructor import BigSMILESConstructor
-from bigsmiles.errors import BigSMILESError
-from bigsmiles.bigsmiles import Atom, Bond, StochasticFragment, BondDescriptorAtom
-
-
-ATOM_PATTERN = re.compile(
-    r"(?P<isotope>\d{1,3})?" +
-    r'(?P<element>' + "|".join(Config.elements_ordered) + "|".join(Config.aromatic) + ')' +
-    r"(?P<stereo>@{0,2})?" +
-    r'(?P<hydrogens>H\d?)?' +
-    r'(?P<charge>[-|+]{1,3}\d?)?'
-)
-
-
-def atom_symbol_to_attributes(symbol: str) -> dict:
-    return ATOM_PATTERN.match(symbol).groupdict()
-
-
-DEFAULT_BONDING_DESCRIPTROR_INDEX = 1
-
-
-def process_bonding_descriptor_symbol(symbol: str) -> tuple[str, int]:
-    symbol = symbol.replace("[", "").replace("]", "")
-    if not symbol:
-        return symbol, DEFAULT_BONDING_DESCRIPTROR_INDEX
-
-    if symbol[-1].isdigit():
-        return symbol[0], int(symbol[-1])
-
-    return symbol, DEFAULT_BONDING_DESCRIPTROR_INDEX
-
-
-def in_stochastic_object(func):
-    """ Decorator to ensure function call only occurs within stochastic object."""
-    @wraps(func)
-    def _in_stochastic_object(*args, **kwargs):
-        if not args[0].stack[-1].in_stochastic_object:
-            raise BigSMILESError("Must in stochastic object.")
-        return func(*args, **kwargs)
-
-    return _in_stochastic_object
-
-
-class BigSMILESStringConstructor(BigSMILESConstructor):
-
-    def add_atom_str(self, symbol: str) -> Atom:
-        return super().add_atom(**atom_symbol_to_attributes(symbol))
-
-    @in_stochastic_object
-    def add_bonding_descriptor_str(self, bd_symbol: str) -> BondDescriptorAtom:
-        return super().add_bonding_descriptor(*process_bonding_descriptor_symbol(bd_symbol))
-
-    def add_bond_atom_pair_str(self, bond_symbol: str, atom_symbol: str) -> tuple[Bond, Atom]:
-        return super().add_bond_atom_pair(bond_symbol, **atom_symbol_to_attributes(atom_symbol))
-
-    @in_stochastic_object
-    def add_bond_bonding_descriptor_pair_str(self, bond_symbol: str, bd_symbol: str) -> tuple[Bond, BondDescriptorAtom]:
-        return super().add_bond_bonding_descriptor_pair(bond_symbol, *process_bonding_descriptor_symbol(bd_symbol))
-
-    def open_stochastic_object_str(self, bd_symbol: str) -> StochasticFragment:
-        return super().open_stochastic_object(*process_bonding_descriptor_symbol(bd_symbol))
-
-    def open_stochastic_object_with_bond_str(self, bond_symbol: str, bd_symbol: str) -> StochasticFragment:
-        return super().open_stochastic_object_with_bond(bond_symbol, *process_bonding_descriptor_symbol(bd_symbol))
-
-    def close_stochastic_object_str(self, bd_symbol: str):
-        return super().close_stochastic_object(*process_bonding_descriptor_symbol(bd_symbol))
-
-    @in_stochastic_object
-    def open_stochastic_fragment(self) -> StochasticFragment:
-        return super().open_stochastic_fragment()
-
-    @in_stochastic_object
-    def close_open_stochastic_fragment(self) -> StochasticFragment:
-        return super().close_open_stochastic_fragment()
-
-    @in_stochastic_object
-    def close_stochastic_fragment(self):
-        return super().close_stochastic_fragment()
-
diff --git a/src/bigsmiles/constructors/__init__.py b/src/bigsmiles/constructors/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bigsmiles/constructors/constructor.py b/src/bigsmiles/constructors/constructor.py
new file mode 100644
index 0000000..99964a5
--- /dev/null
+++ b/src/bigsmiles/constructors/constructor.py
@@ -0,0 +1,1095 @@
+"""
+Constructor:
+
+This file contains functions that can be called to construct a BigSMILES object.
+
+There are two approaches:
+* build it step by step;  where you open branch add atoms then close branch
+* build it in chunks;  where you build the branch in a separate BigSMILES object and attach it.
+
+"""
+from __future__ import annotations
+import logging
+
+import bigsmiles.errors as errors
+import bigsmiles.reference_data.chemical_data as chemical_data
+from bigsmiles.data_structures.bigsmiles import Atom, Bond, BondDescriptor, Branch, StochasticFragment, \
+    StochasticObject, BigSMILES, BondDescriptorAtom, has_node_attr, has_ring_attr, has_parent_attr
+import bigsmiles.validation.validation_bigsmiles_obj as validation_bigsmiles_obj
+
+
+## Utility functions for construction ## noqa
+######################################################################################################################
+def add_bond_to_connected_objects(bond: Bond):
+    """
+    Adds Bond to Atoms.bonds attribute
+    also works on BondDescriptorAtom and stochastic objects
+    """
+    if bond.ring_id is not None and isinstance(bond.atom2, StochasticObject):
+        atoms = reversed(bond)
+    else:
+        atoms = iter(bond)
+
+    for i, obj in enumerate(atoms):
+        if isinstance(obj, Atom):
+            obj.bonds += [bond]
+        elif isinstance(obj, BondDescriptorAtom):
+            obj.bond = bond
+        elif isinstance(obj, StochasticObject):
+            if i == 0:
+                obj.bond_right = bond
+            else:
+                obj.bond_left = bond
+
+
+def add_bond_descriptor_to_stochastic_fragment(stoch_frag: StochasticFragment, loop_obj: Branch = None):
+    """ Add bonding descriptor to stoch_frag.bonding_descriptors. Recursive. """
+    if loop_obj is None:
+        loop_obj = stoch_frag
+
+    for obj in loop_obj.nodes:
+        if isinstance(obj, BondDescriptorAtom) and obj.descriptor not in stoch_frag.bonding_descriptors:
+            stoch_frag.bonding_descriptors.append(obj.descriptor)
+        if isinstance(obj, Branch):
+            add_bond_descriptor_to_stochastic_fragment(stoch_frag, obj)  # recursive
+
+
+def get_common_bond(atom1: Atom, atom2: Atom | StochasticObject) -> Bond | None:
+    """ Checks if the two atoms have a bond in common. """
+    if isinstance(atom2, StochasticObject):
+        if atom2.bond_right is not None and atom2.bond_right in atom1.bonds:
+            raise errors.ConstructorError("An atom can't have two bonds to the same stochastic object.")
+    else:
+        for bond in atom1.bonds:
+            if bond in atom2.bonds:
+                return bond
+
+    return None  # No common bond found
+
+
+def remove_partial_ring(parent: has_ring_attr, ring_id: int):
+    """ Given a ring_id, remove ring from parent. """
+    for i, ring in enumerate(parent.rings):
+        if ring.ring_id == ring_id:
+            bond = parent.rings.pop(i)
+            parent.bonds.remove(bond)
+            if hasattr(parent, "parent"):
+                parent.parent.bonds.remove(bond)
+            break
+
+
+def insert_obj_into_list(list_: list, obj, object_to_insert):
+    """ Insert 'object_to_insert' in a list right behind 'obj'. """
+    index_ = list_.index(obj) + 1
+    for i, item in enumerate(list_[index_:]):
+        if isinstance(item, Branch):  # put it at the end of existing branches
+            continue
+
+        list_.insert(index_ + i, object_to_insert)
+        break
+
+
+def get_prior(obj: has_node_attr, types_: tuple, flag: bool = False) -> Atom:
+    """ Get prior """
+    if obj.nodes:
+        for node in reversed(obj.nodes):  # no atom can ever have more than 10 bonds
+            if type(node) in types_:
+                return node
+
+        raise errors.ConstructorError("Trying to make a bond to a prior atom that isn't there.??")
+
+    elif not flag:
+        return get_prior(obj.parent, types_, flag=True)
+
+    raise errors.ConstructorError("Bond attempted to be made to that has nothing to bond back to.")
+
+
+def get_ring_parent(parent: has_parent_attr) -> BigSMILES | StochasticFragment:
+    """ Recursively find a parent that has the 'ring' attribute. only BigSMILES and Stochastic Fragments do. """
+    if hasattr(parent, "rings"):
+        return parent
+
+    return get_ring_parent(parent.parent)
+
+
+def add_explict_hydrogen_to_stochastic_objects(bigsmiles_: BigSMILES):
+    """ {[$][$]CC[$][$]} --> [H]{[$][$]CC[$][$]}[H] """
+    if isinstance(bigsmiles_.nodes[0], StochasticObject) and not bigsmiles_.nodes[0].implicit_endgroups:
+        if bigsmiles_.nodes[0].bd_left.bond_order > 1:
+            raise errors.ConstructorError(
+                f"Invalid BigSMILES. A double bond coming out of a stochastic object requires an "
+                f"explict endgroup defined. \n Current Bigsmiles: {bigsmiles_}")
+
+        insert_atom_and_bond(bigsmiles_, None, "", "H")
+
+    if isinstance(bigsmiles_.nodes[-1], StochasticObject) and not bigsmiles_.nodes[-1].implicit_endgroups and \
+            bigsmiles_.nodes[-1].bond_right is None:
+        if bigsmiles_.nodes[-1].bd_right.bond_order > 1:
+            raise errors.ConstructorError(
+                f"Invalid BigSMILES. A double bond coming out of a stochastic object requires an "
+                f"explict endgroup defined. \n Current Bigsmiles: {bigsmiles_}")
+
+        add_bond_atom_pair(bigsmiles_, "", "H")
+
+
+def exit_construction(parent: BigSMILES, syntax_fix: bool = True, validation: bool = True):
+    """
+    runs several methods to fix any syntax issues and runs more extensive validation
+
+    Parameters
+    ----------
+    parent:
+        BigSMILES to be checked
+    syntax_fix:
+        True to perform syntax fixes
+    validation:
+        True to run validation
+    """
+    if not isinstance(parent, BigSMILES):
+        raise errors.ConstructorError(f"{type(parent)} is missing closing symbol.")
+
+    if not parent:
+        return parent
+    if syntax_fix:
+        add_explict_hydrogen_to_stochastic_objects(parent)
+        validation_bigsmiles_obj.run_syntax_fixes(parent)
+    if validation:
+        validation_bigsmiles_obj.run_validation(parent)
+
+
+## Functions for constructing a bigsmiles sequentially or step by step ## noqa
+#######################################################################################################################
+def add_atom(
+        parent: has_node_attr,
+        symbol: str,
+        isotope: int | None = None,
+        stereo: str | None = None,
+        hydrogens: int | None = None,
+        charge: int = 0,
+        valence: int | None = None,
+        class_: int | None = None,
+        **kwargs
+) -> has_node_attr:
+    """
+    appends an `Atom` to the end of the `parent`
+
+    Parameters
+    ----------
+    parent:
+    symbol:
+        symbol symbol (e.g., H, C, O, Zn)
+    isotope:
+        isotope (e.g., [13C])
+    stereo:
+        stereochemistry [None, "@", "@@"] (None means not defined)
+    hydrogens:
+        number of explict hydrogens  (e.g., [CH2])
+    charge:
+        symbol charge  (e.g., [Fe+3])
+    valence:
+        The capacity to form bonds with other atoms
+    class_:
+        index of class (e.g., [C:1] class_ = 1)
+    kwargs:
+        any additional keyword arguments are accepted and set as additional attributes to the Atom
+
+    Returns
+    -------
+    parent:
+        returns the parent passed to it
+    """
+    atom = Atom(parent._get_id(Atom), symbol, isotope, stereo, hydrogens, charge, valence, class_,
+                parent=parent, **kwargs)
+    parent.nodes.append(atom)
+    parent.root.atoms.append(atom)
+
+    return parent
+
+
+def add_bond(
+        parent: has_node_attr,
+        bond_symbol: str | None,
+        atom1: Atom | BondDescriptorAtom,
+        atom2: Atom | BondDescriptorAtom | StochasticObject | None,
+        **kwargs
+) -> has_node_attr:
+    """
+    add 'Bond' between two 'Atoms' (typically not be used directly)
+
+    Parameters
+    ----------
+    parent:
+    bond_symbol:
+        bond symbol ("", "=", "#", etc.)
+    atom1:
+        atom the bond starts at
+    atom2:
+        atom the bond ends at
+    kwargs:
+        any additional keyword arguments are accepted and set as additional attributes to the Bond
+
+    Returns
+    -------
+    parent:
+        returns the parent passed to it
+    """
+    bond = Bond(parent._get_id(Bond), bond_symbol, atom1, atom2, parent=parent, **kwargs)
+    parent.nodes.append(bond)
+    parent.root.bonds.append(bond)
+    add_bond_to_connected_objects(bond)
+
+    return parent
+
+
+def add_ring_by_index(parent: has_node_attr, ring_id: int, bond_symbol: str | None, **kwargs) -> has_node_attr:
+    """
+    add ring by index; rings are just 'Bond'
+
+    !!! warning
+
+        This function needs to be called twice to work; once at each time the parsing hits a ring index.
+
+        'exit_construction()' will check to confirm that there is no uncompleted rings
+
+    Parameters
+    ----------
+    parent:
+    ring_id:
+        index of ring
+    bond_symbol:
+        bond symbol ("", "=", "#", etc.)
+    kwargs:
+        any additional keyword arguments are accepted and set as additional attributes to the Bond
+
+    Returns
+    -------
+    parent:
+        returns the parent passed to it
+    """
+    # check if ring_id already exists
+    ring_parent = get_ring_parent(parent)
+    for ring in ring_parent.rings:
+        if ring.ring_id == ring_id:
+            if ring.atom2 is not None:
+                raise errors.ConstructorError(f"Ring already formed for ring id {ring_id}.")
+            atom2 = get_prior(parent, (Atom, StochasticObject))
+
+            # validation
+            bond = get_common_bond(ring.atom1, atom2)
+            if bond:
+                logging.warning("Duplicate ring detected and merged into one with a higher bond order. ")
+                bond.bond_order += chemical_data.bond_mapping[bond_symbol]
+                remove_partial_ring(ring_parent, ring_id)
+                return parent
+
+            # Add atom2
+            # ring.atom1, ring.atom2 = atom2, ring.atom1
+            ring.atom2 = atom2
+
+            # this is to handle that the multi-bond symbol (-, #) could be on either or both ring index
+            if chemical_data.bond_mapping[bond_symbol] > ring.bond_order:
+                ring.symbol = bond_symbol
+
+            # aromatic_elements ring closure
+            if ring.atom1.aromatic and ring.atom2.aromatic:
+                ring.symbol = ":"
+
+            add_bond_to_connected_objects(ring)
+            return parent
+
+    # Make new ring_id
+    bond = Bond(parent._get_id(Bond), bond_symbol, get_prior(parent, (Atom, StochasticObject)), None, ring_id,
+                parent=parent, **kwargs)
+    parent.root.bonds.append(bond)
+    ring_parent.rings.append(bond)
+
+    return parent
+
+
+def add_ring_from_atoms(parent: has_node_attr, atom1: Atom, atom2: Atom, bond_symbol: str | None = "", **kwargs) \
+        -> has_node_attr:
+    """
+    add ring from with both 'Atoms'; rings are just 'Bond'
+
+    Parameters
+    ----------
+    parent:
+    atom1:
+        atom the ring starts at
+    atom2:
+        atom the ring ends at
+    bond_symbol:
+        bond symbol ("", "=", "#", etc.)
+    kwargs
+        any additional keyword arguments are accepted and set as additional attributes to the Bond
+
+    Returns
+    -------
+    parent:
+        returns the parent passed to it
+    """
+    ring_parent = get_ring_parent(parent)
+    bond = get_common_bond(atom1, atom2)  # Check if ring already between two atoms
+    if bond is None:
+        # Make new ring_id
+        bond = Bond(parent._get_id(Bond), bond_symbol, atom1, atom2, len(parent.root.rings) + 1,
+                    parent=parent, **kwargs)
+        parent.root.bonds.append(bond)
+        ring_parent.rings.append(bond)
+        add_bond_to_connected_objects(bond)
+    else:
+        # increase bond order of existing bond
+        bond.bond_order += 1
+
+    return parent
+
+
+def _get_bonding_descriptor_atom(parent, descriptor: str, index_: int, bond_symbol: str | None, **kwargs) \
+        -> BondDescriptorAtom:
+    stoch_obj = _get_current_stochastic_object(parent)
+    bd = _get_bonding_descriptor(stoch_obj, descriptor, index_, bond_symbol)
+    return BondDescriptorAtom(parent._get_id(BondDescriptorAtom), bd, parent=parent, **kwargs)
+
+
+def _get_bonding_descriptor(stoch_obj: StochasticObject, descriptor: str, index_: int, bond_symbol: str | None) \
+        -> BondDescriptor:
+    # check if bd in there already
+    for bd in stoch_obj.bonding_descriptors:
+        if descriptor == bd.descriptor and index_ == bd.index_:
+            if bond_symbol is not None and bd.bond_symbol != bond_symbol:
+                raise errors.ConstructorError("Multiple bond orders to same bonding descriptor.")
+                # logging.warning("Multiple bond orders to same bonding descriptor.")
+            return bd
+
+    # create new bonding descriptor
+    new_bd = BondDescriptor(stoch_obj, descriptor, index_, bond_symbol)
+    stoch_obj.bonding_descriptors.append(new_bd)
+    return new_bd
+
+
+def _get_current_stochastic_object(parent) -> StochasticObject:
+    """ Recursive"""
+    if isinstance(parent, StochasticObject):
+        return parent
+
+    if hasattr(parent, "parent"):
+        return _get_current_stochastic_object(parent.parent)
+    else:
+        raise errors.ConstructorError("No stochastic object found.")
+
+
+def add_bonding_descriptor_atom(parent, descriptor: str, index_: int, bond_symbol: str | None = None, **kwargs) \
+        -> has_node_attr:
+    """
+    appends 'BondDescriptorAtom' to the end of the `parent`
+
+    Parameters
+    ----------
+    parent:
+    bond_symbol:
+        bond symbol ("", "=", "#", etc.)
+    descriptor:
+        bonding descriptor symbol [<, >, $]
+    index_:
+        bonding descriptor index [1, inf]
+    kwargs
+        any additional keyword arguments are accepted and set as additional attributes to the BondDescriptorAtom
+
+    Returns
+    -------
+    parent:
+        returns the parent passed to it
+    """
+    bd_atom = _get_bonding_descriptor_atom(parent, descriptor, index_, bond_symbol, **kwargs)
+    parent.nodes.append(bd_atom)
+    parent.root.atoms.append(bd_atom)
+    return parent
+
+
+def add_bond_atom_pair(
+        parent: has_node_attr,
+        bond_symbol: str | None,
+        symbol: str,
+        isotope: int | None = None,
+        stereo: str | None = None,
+        hydrogens: int | None = None,
+        charge: int = 0,
+        valence: int = None,
+        class_: int | None = None,
+        kwargs_atom: dict = None,
+        kwargs_bond: dict = None
+) -> has_node_attr:
+    """
+    appends a `Bond` followed by an `Atom`
+
+    Parameters
+    ----------
+    parent:
+        parent
+    bond_symbol:
+        bond symbol ("", "=", "#", etc.)
+    symbol:
+        symbol symbol (e.g., H, C, O, Zn)
+    isotope:
+        isotope (e.g., [13C])
+    stereo:
+        stereochemistry [None, "@", "@@"] (None means not defined)
+    hydrogens:
+        number of explict hydrogens  (e.g., [CH2])
+    charge:
+        symbol charge  (e.g., [Fe+3])
+    valence:
+        The capacity to form bonds with other atoms
+    class_:
+        index of class (e.g., [C:1] class_ = 1)
+    kwargs_atom:
+        any additional keyword arguments are accepted and set as additional attributes to the Atom
+    kwargs_bond
+        any additional keyword arguments are accepted and set as additional attributes to the Bond
+
+    Returns
+    -------
+    parent:
+        returns the parent passed to it
+    """
+    kwargs_atom = kwargs_atom if kwargs_atom is not None else {}
+    kwargs_bond = kwargs_bond if kwargs_bond is not None else {}
+
+    atom = Atom(parent._get_id(Atom), symbol, isotope, stereo, hydrogens, charge, valence, class_,
+                parent=parent, **kwargs_atom)
+    prior_atom = get_prior(parent, (Atom, BondDescriptorAtom, StochasticObject))
+    add_bond(parent, bond_symbol, prior_atom, atom, **kwargs_bond)
+    parent.nodes.append(atom)
+    parent.root.atoms.append(atom)
+
+    return parent
+
+
+def add_bond_bonding_descriptor_pair(
+        parent: StochasticFragment | Branch,
+        bond_symbol: str | None,
+        descriptor: str,
+        index_: int,
+        kwargs_bond_descriptor: dict = None,
+        kwargs_bond: dict = None
+) -> StochasticFragment | Branch:
+    """
+    appends a `Bond` followed by an `BondingDescriptorAtom`
+
+    Parameters
+    ----------
+    parent:
+    bond_symbol:
+        bond symbol ("", "=", "#", etc.)
+    descriptor:
+        bonding descriptor symbol [<, >, $]
+    index_:
+        bonding descriptor index [1, inf]
+    kwargs_bond_descriptor:
+        any additional keyword arguments are accepted and set as additional attributes to the BondDescriptorAtom
+    kwargs_bond:
+        any additional keyword arguments are accepted and set as additional attributes to the Bond
+
+    Returns
+    -------
+    parent:
+        returns the parent passed to it
+    """
+    kwargs_bond_descriptor = kwargs_bond_descriptor if kwargs_bond_descriptor is not None else {}
+    kwargs_bond = kwargs_bond if kwargs_bond is not None else {}
+
+    bd_atom = _get_bonding_descriptor_atom(parent, descriptor, index_, bond_symbol, **kwargs_bond_descriptor)
+    prior_atom = get_prior(parent, (Atom, BondDescriptorAtom, StochasticObject))
+    add_bond(parent, bond_symbol, prior_atom, bd_atom, **kwargs_bond)
+    parent.nodes.append(bd_atom)
+    parent.root.atoms.append(bd_atom)
+    return parent
+
+
+def open_branch(parent: has_node_attr, **kwargs) -> Branch:
+    """
+    open branch
+
+    !!! warning
+
+        close_branch() must be called at some later point
+
+    Parameters
+    ----------
+    parent:
+    kwargs:
+        any additional keyword arguments are accepted and set as additional attributes to the Branch
+
+    Returns
+    -------
+    parent:
+        returns Branch
+    """
+    if len(parent.nodes) == 0 and hasattr(parent, 'parent') and isinstance(parent.parent, Branch):
+        raise errors.ConstructorError("BigSMILES string or branch can't start with branch")
+
+    branch = Branch(parent._get_id(Branch), parent, **kwargs)
+    parent.nodes.append(branch)
+
+    return branch
+
+
+def close_branch(parent: Branch) -> has_node_attr:
+    """
+    closes branch and appends it to `parent.parent`
+
+    Parameters
+    ----------
+    parent:
+
+    Returns
+    -------
+    parent:
+        returns parent.parent
+
+    """
+    if not isinstance(parent, Branch):
+        raise errors.ConstructorError("Error closing branch. Possible issues: "
+                                      "\n\tClosing a branch with another intermediate node started."
+                                      "\n\tNo starting branch symbol.")
+
+    # check if branch is empty; if so remove it
+    if len(parent.nodes) == 0:
+        parent.parent.nodes.pop()
+
+    return parent.parent
+
+
+def open_stochastic_object(parent: has_node_attr, descriptor: str, index_: int, **kwargs) -> StochasticObject:
+    """
+    open `StochasticObject`
+
+    !!! warning
+
+        close_stochastic_object() must be called at a later point
+
+    Parameters
+    ----------
+    parent:
+    descriptor:
+        bonding descriptor symbol [<, >, $]
+    index_:
+        bonding descriptor index [1, inf]
+    kwargs
+        any additional keyword arguments are accepted and set as additional attributes to the BondDescriptorAtom
+
+    Returns
+    -------
+    parent:
+        returns StochasticObject
+    """
+    stoch_obj = StochasticObject(parent._get_id(StochasticObject), parent, **kwargs)
+    stoch_obj.bd_left = _get_bonding_descriptor(stoch_obj, descriptor, index_, None)
+    parent.nodes.append(stoch_obj)
+
+    return stoch_obj
+
+
+def open_stochastic_object_fragment(parent: has_node_attr, descriptor: str, index_: int, **kwargs) \
+        -> StochasticFragment:
+    """
+    opens `StochasticObject` and `StochasticFragment`
+
+    !!! warning
+
+        close_stochastic_fragment() must be called at a later point
+
+        close_stochastic_object() must be called at a later point
+
+    Parameters
+    ----------
+    parent:
+    descriptor:
+        bonding descriptor symbol [<, >, $]
+    index_:
+        bonding descriptor index [1, inf]
+    kwargs
+        any additional keyword arguments are accepted and set as additional attributes to the BondDescriptorAtom
+
+    Returns
+    -------
+    parent:
+        returns StochasticFragment
+    """
+    stoch_obj = open_stochastic_object(parent, descriptor, index_, **kwargs)
+
+    return open_stochastic_fragment(stoch_obj)
+
+
+def open_stochastic_object_fragment_with_bond(parent: has_node_attr, bond_symbol: str | None, descriptor: str,
+                                              index_: int, **kwargs) -> StochasticFragment:
+    """
+    opens `StochasticObject` and `StochasticFragment`
+
+    !!! warning
+
+        close_stochastic_fragment() must be called at a later point
+
+        close_stochastic_object() must be called at a later point
+
+    Parameters
+    ----------
+    parent:
+    bond_symbol:
+        bond symbol ("", "=", "#", etc.)
+    descriptor:
+        bonding descriptor symbol [<, >, $]
+    index_:
+        bonding descriptor index [1, inf]
+    kwargs
+        any additional keyword arguments are accepted and set as additional attributes to the BondDescriptorAtom
+
+    Returns
+    -------
+    parent:
+        returns StochasticFragment
+    """
+    stoch_obj = StochasticObject(parent._get_id(StochasticObject), parent, **kwargs)
+    stoch_obj.bd_left = _get_bonding_descriptor(stoch_obj, descriptor, index_, bond_symbol)
+
+    # bond made without 'add_bond' function to ensure it's added to 'bond_left'
+    prior_atom = get_prior(parent, (Atom, StochasticObject))
+    bond = Bond(parent._get_id(Bond), bond_symbol, prior_atom, stoch_obj, parent=parent)
+    parent.nodes.append(bond)
+    parent.root.bonds.append(bond)
+    add_bond_to_connected_objects(bond)
+    parent.nodes.append(stoch_obj)
+
+    return open_stochastic_fragment(stoch_obj)
+
+
+def close_stochastic_object(parent: StochasticObject, descriptor: str, index_: int, bond_symbol: str | None) \
+        -> has_node_attr:
+    """
+    closes `StochasticObject`
+
+    Parameters
+    ----------
+    parent:
+    descriptor:
+        bonding descriptor symbol [<, >, $]
+    index_:
+        bonding descriptor index [1, inf]
+    bond_symbol:
+        bond symbol ("", "=", "#", etc.)
+
+    Returns
+    -------
+    parent:
+        returns parent.parent
+
+    Raises
+    ------
+    ConstructorError
+        Error closing StochasticObject. Possible issues:
+
+        * Closing a StochasticObject with another intermediate node started.
+        * No starting StochasticObject symbol.
+    """
+    if not isinstance(parent, StochasticObject):
+        raise errors.ConstructorError("Error closing StochasticObject. Possible issues: "
+                                      "\n\tClosing a StochasticObject with another intermediate node started."
+                                      "\n\tNo starting StochasticObject symbol.")
+
+    parent.bd_right = _get_bonding_descriptor(parent, descriptor, index_, bond_symbol)
+    return parent.parent
+
+
+def open_stochastic_fragment(parent: StochasticObject) -> StochasticFragment:
+    """
+    open `StochasticFragment`
+
+    !!! warning
+
+        close_stochastic_fragment() must be called at a later point
+
+
+    Parameters
+    ----------
+    parent:
+
+    Returns
+    -------
+    parent:
+        returns StochasticFragment
+    """
+    stoch_frag = StochasticFragment(parent._get_id(StochasticFragment), parent)
+    parent.nodes.append(stoch_frag)
+    return stoch_frag
+
+
+def close_open_stochastic_fragment(parent: StochasticFragment) -> StochasticFragment:
+    """
+
+    closes `StochasticFragment` and opens a new `StochasticFragment`
+
+    Parameters
+    ----------
+    parent:
+
+    Returns
+    -------
+    parent:
+        returns a new StochasticFragment
+    """
+    if not isinstance(parent, StochasticFragment):
+        raise errors.ConstructorError("Stochastic seperator can only follow a stochastic fragments.")
+
+    parent = close_stochastic_fragment(parent)
+    return open_stochastic_fragment(parent)
+
+
+def close_stochastic_fragment(parent: StochasticFragment) -> StochasticObject:
+    """
+
+    Parameters
+    ----------
+    parent:
+
+    Returns
+    -------
+    parent:
+        returns parent.parent
+    """
+    if not isinstance(parent, StochasticFragment):
+        raise errors.ConstructorError("Error StochasticFragment branch. Possible issues: "
+                                      "\n\tClosing a branch with another intermediate node started.")
+
+    add_bond_descriptor_to_stochastic_fragment(parent)
+
+    # check for at-least one bonding descriptor
+    if not parent.bonding_descriptors:
+        raise errors.ConstructorError(f"No bonding descriptor in the stochastic fragment. ({repr(parent)})")
+
+    return parent.parent
+
+
+## functions for building BigSMILES in chunks ## noqa
+###################################################################################################################
+def append_bigsmiles_fragment(parent, bigsmiles_: BigSMILES, bond_symbol: str | None, **kwargs) -> BigSMILES:
+    """
+
+    Parameters
+    ----------
+    parent
+    bigsmiles_
+    bond_symbol
+    kwargs
+
+    Returns
+    -------
+
+    """
+    if not isinstance(bigsmiles_.nodes[0], (Atom, StochasticObject)):
+        raise errors.ConstructorError("First node must be an 'Atom' or 'StochasticObject' to added fragment.")
+    if not parent:
+        return bigsmiles_
+
+    atom = bigsmiles_.nodes[0]
+    prior_atom = get_prior(parent, (Atom, StochasticObject))
+    add_bond(parent, bond_symbol, prior_atom, atom, **kwargs)
+
+    set_new_parent(parent, bigsmiles_)
+
+    # append fragment
+    parent.nodes += bigsmiles_.nodes
+    parent.root.atoms += bigsmiles_.atoms
+    parent.root.bonds += bigsmiles_.bonds
+    parent.root.rings += bigsmiles_.rings
+
+    return parent
+
+
+def attach_bigsmiles_branch(parent, bond_symbol: str | None | None, bigsmiles_: BigSMILES, index_: int,
+                            kwargs_bond: dict = None, kwargs_branch: dict = None):
+    """
+
+    Parameters
+    ----------
+    parent
+    bond_symbol
+    bigsmiles_
+    index_
+    kwargs_bond
+    kwargs_branch
+
+    Returns
+    -------
+
+    """
+    kwargs_bond = kwargs_bond if kwargs_bond is not None else {}
+    kwargs_branch = kwargs_branch if kwargs_branch is not None else {}
+
+    if index_ > len(parent.root.atoms) - 1:
+        raise errors.ConstructorError(f"Branch can't be added. 'index' outside atom list."
+                                      f"\n current smiles: {parent.root} \n desired atom index: {index_}")
+
+    set_new_parent(parent, bigsmiles_)
+
+    # make branch
+    branch = Branch(parent._get_id(Branch), parent, **kwargs_branch)
+    branch.nodes = bigsmiles_.nodes
+    parent.root.atoms += bigsmiles_.atoms
+    parent.root.bonds += bigsmiles_.bonds
+
+    # make bond
+    atom = parent.root.atoms[index_]
+    bond = Bond(parent._get_id(Bond), bond_symbol, atom, branch.nodes[0], parent=parent, **kwargs_bond)
+    branch.nodes.insert(0, bond)
+    parent.root.bonds.append(bond)
+    add_bond_to_connected_objects(bond)
+
+    # insert branch (put it after all the other branches on the atom)
+    insert_obj_into_list(parent.root.nodes, atom, branch)
+
+    return parent
+
+
+def insert_atom_and_bond(
+        parent: has_node_attr,
+        prior_atom: Atom | BondDescriptorAtom | Branch | StochasticObject | None,
+        bond_symbol: str | None,
+        element: str,
+        isotope: int | None = None,
+        stereo: str = '',
+        hydrogens: int = 0,
+        charge: int = 0,
+        valance: int = None,
+        class_: int | None = None,
+        kwargs_atom: dict = None,
+        kwargs_bond: dict = None
+) -> has_node_attr:
+    """
+
+    Parameters
+    ----------
+    parent
+    prior_atom
+    bond_symbol
+    element
+    isotope
+    stereo
+    hydrogens
+    charge
+    valance
+    class_
+    kwargs_atom
+    kwargs_bond
+
+    Returns
+    -------
+
+    """
+    kwargs_atom = kwargs_atom if kwargs_atom is not None else {}
+    kwargs_bond = kwargs_bond if kwargs_bond is not None else {}
+
+    atom = Atom(parent._get_id(Atom), element, isotope, stereo, hydrogens, charge, valance, class_,
+                parent=parent, **kwargs_atom)
+
+    if prior_atom is None and isinstance(parent, BigSMILES):  # add to beginning
+        prior_atom = parent.nodes[0]
+        bond = Bond(parent._get_id(Bond), bond_symbol, atom, prior_atom, parent=parent, **kwargs_bond)
+        add_bond_to_connected_objects(bond)
+        parent.nodes.insert(0, bond)
+        parent.root.bonds.insert(0, bond)
+        parent.nodes.insert(0, atom)
+        parent.root.atoms.insert(0, atom)
+
+    elif prior_atom == prior_atom.parent.nodes[-1]:  # add to end
+        bond = Bond(parent._get_id(Bond), bond_symbol, parent.atoms[-1], atom, parent=parent, **kwargs_bond)
+        add_bond_to_connected_objects(bond)
+        parent.nodes.append(bond)
+        parent.root.bonds.append(bond)
+        parent.nodes.append(atom)
+        parent.root.atoms.append(atom)
+
+    validation_bigsmiles_obj.re_number_node_ids(parent.root)
+    return parent
+
+
+def insert_atom_into_bond(
+        parent: has_node_attr,
+        bond_to_insert_into: Bond,
+        element: str,
+        isotope: int | None = None,
+        stereo: str = '',
+        hydrogens: int = 0,
+        charge: int = 0,
+        valance: int = None,
+        class_: int | None = None,
+        kwargs_atom: dict = None,
+) -> has_node_attr:
+    """
+
+    Parameters
+    ----------
+    parent
+    bond_to_insert_into
+    element
+    isotope
+    stereo
+    hydrogens
+    charge
+    valance
+    class_
+    kwargs_atom
+
+    Returns
+    -------
+
+    """
+    kwargs_atom = kwargs_atom if kwargs_atom is not None else {}
+
+    # create atom
+    atom = Atom(parent._get_id(Atom), element, isotope, stereo, hydrogens, charge, valance, class_,
+                parent=parent, **kwargs_atom)
+
+    # create bonds
+    new_bond1 = Bond(parent._get_id(Bond), bond_to_insert_into.symbol, bond_to_insert_into.atom1, atom, parent=parent)
+    new_bond2 = Bond(parent._get_id(Bond), bond_to_insert_into.symbol, atom, bond_to_insert_into.atom2, parent=parent)
+
+    # remove old bond and add new bonds and atoms to parent
+    bond_index = parent.nodes.index(bond_to_insert_into)
+    parent.nodes.insert(bond_index + 1, new_bond1)
+    parent.nodes.insert(bond_index + 2, atom)
+    parent.nodes.insert(bond_index + 3, new_bond2)
+    parent.nodes.pop(bond_index)
+
+    # remove old bond and add new bonds and atoms to root
+    bond_index = parent.root.bonds.index(bond_to_insert_into)
+    parent.root.bonds.insert(bond_index + 1, new_bond1)
+    parent.root.bonds.insert(bond_index + 2, new_bond2)
+    parent.root.bonds.pop(bond_index)
+    atom_index = parent.root.atoms.index(bond_to_insert_into.atom1)
+    parent.root.atoms.insert(atom_index + 1, atom)
+
+    # remove old bond from atoms and add new bonds to atoms
+    bond_to_insert_into.atom1.bonds.remove(bond_to_insert_into)
+    bond_to_insert_into.atom2.bonds.remove(bond_to_insert_into)
+    add_bond_to_connected_objects(new_bond1)
+    add_bond_to_connected_objects(new_bond2)
+
+    return parent
+
+
+def add_bonding_descriptor_bond_via_index(
+        parent: StochasticFragment,
+        bond_symbol: str | None,
+        descriptor: str,
+        index_: int,
+        prior_atom: Atom,
+        kwargs_bond: dict = None,
+):
+    """
+
+    Parameters
+    ----------
+    parent
+    bond_symbol
+    descriptor
+    index_
+    prior_atom
+    kwargs_bond
+
+    Returns
+    -------
+
+    """
+    kwargs_bond = kwargs_bond if kwargs_bond is not None else {}
+
+    bd_atom = _get_bonding_descriptor_atom(parent, descriptor, index_, bond_symbol)
+    bond = Bond(parent._get_id(Bond), bond_symbol, prior_atom, bd_atom, parent=parent, **kwargs_bond)
+    add_bond_to_connected_objects(bond)
+
+    if prior_atom == prior_atom.parent.nodes[0]:  # if first atom
+        parent.nodes.insert(0, bond)
+        parent.root.bonds.insert(0, bond)
+        parent.nodes.insert(0, bd_atom)
+        parent.root.atoms.insert(0, bd_atom)
+    elif prior_atom == prior_atom.parent.nodes[-1] or \
+            all([False for node in parent.nodes[parent.nodes.index(prior_atom) + 1:] if not isinstance(node, Branch)]):
+        # if last atom or last atom followed by only branches
+        parent.nodes.append(bond)
+        parent.root.bonds.append(bond)
+        parent.nodes.append(bd_atom)
+        parent.root.atoms.append(bd_atom)
+    else:
+        branch = Branch(parent._get_id(Branch), parent)
+        branch.nodes = [bond, bd_atom]
+        insert_obj_into_list(parent.nodes, prior_atom, branch)
+        insert_obj_into_list(parent.root.bonds, prior_atom.bonds[0], bond)
+        insert_obj_into_list(parent.root.atoms, prior_atom, bd_atom)
+
+
+def add_stochastic_fragment(stoch_object: StochasticObject, stoch_fragment: StochasticFragment) -> StochasticObject:
+    """
+
+    Parameters
+    ----------
+    stoch_object
+    stoch_fragment
+
+    Returns
+    -------
+
+    """
+    stoch_object.nodes.append(stoch_fragment)
+    stoch_fragment.parent = stoch_object
+
+    for node in stoch_fragment.nodes:
+        if isinstance(node, Atom):
+            stoch_object.root.atoms.append(node)
+        elif isinstance(node, Bond):
+            stoch_object.root.bonds.append(node)
+        elif isinstance(node, BondDescriptorAtom):
+            # re-direct descriptors to the one in the stoch_object
+            for descriptor in stoch_object.bonding_descriptors:
+                if descriptor == node.descriptor:
+                    node.descriptor = descriptor
+                    continue
+
+            # if descriptor not found already in stochastic object, add it
+            stoch_object.bonding_descriptors.append(node.descriptor)
+
+    return stoch_object
+
+
+def add_bigsmiles_as_stochastic_fragment(stoch_obj: StochasticObject, bigsmiles_: BigSMILES):
+    """
+
+    Parameters
+    ----------
+    stoch_obj
+    bigsmiles_
+
+    Returns
+    -------
+
+    """
+    stoch_fragment = StochasticFragment(stoch_obj.root._get_id(StochasticFragment), stoch_obj)
+    stoch_fragment.nodes = bigsmiles_.nodes
+    stoch_fragment.rings = bigsmiles_.rings
+    stoch_fragment.bonding_descriptors = \
+        list({node.descriptor for node in bigsmiles_.nodes if isinstance(node, BondDescriptorAtom)})
+
+    return add_stochastic_fragment(stoch_obj, stoch_fragment)
+
+
+def set_new_parent(new_parent, obj):
+    """ re-direct 'parent' to new bigsmiles object """
+    for node in obj.nodes:
+        if hasattr(node, 'parent'):
+            node.parent = new_parent
+
+
+__all__ = ["has_node_attr", "add_atom", "add_bond", "add_ring_by_index", "add_ring_from_atoms", "add_bonding_descriptor_atom",
+           "add_bond_atom_pair", "add_bond_bonding_descriptor_pair", "open_branch", "close_branch",
+           "open_stochastic_object", "open_stochastic_object_fragment", "open_stochastic_object_fragment_with_bond",
+           "close_stochastic_object", "open_stochastic_fragment", "close_open_stochastic_fragment",
+           "close_stochastic_fragment", "exit_construction", "get_prior"
+           ]
diff --git a/src/bigsmiles/constructors/constructor_reaction.py b/src/bigsmiles/constructors/constructor_reaction.py
new file mode 100644
index 0000000..fe77653
--- /dev/null
+++ b/src/bigsmiles/constructors/constructor_reaction.py
@@ -0,0 +1,264 @@
+from __future__ import annotations
+import re
+
+import bigsmiles.errors as errors
+from bigsmiles.data_structures.bigsmiles import BigSMILES, Bond, Atom
+
+
+rxn_split_pattern = re.compile(r"(?<!\[)(>>|>)")
+
+
+def parse_reaction(text: str) -> tuple[list[BigSMILES], list[BigSMILES], list[BigSMILES]]:
+    """
+    Main entry point to parsing reaction SMILES.
+
+    !!! info "Regular Expression Pattern"
+
+        pattern: r"(?<!\[)(>>|>)"
+
+    Parameters
+    ----------
+    text: str
+        reaction BigSMILES string
+
+    Returns
+    -------
+    reactants: list[BigSMILES]
+        reactants
+    agents: list[BigSMILES]
+        agents
+    products: list[BigSMILES]
+        products
+    """
+    result = re.split(rxn_split_pattern, text.replace(" ", ""))
+
+    try:
+        if ">>" in result:
+            return parse_no_agent_reaction(result)
+        return parse_agent_reaction(result)
+    except errors.BigSMILESError as e:
+        raise errors.BigSMILESError(f"Parsing failed on '{text}'.", e) from e
+
+
+def parse_no_agent_reaction(text_list: list[str]) -> tuple[list[BigSMILES], list[BigSMILES], list[BigSMILES]]:
+    """ the pattern should be [reactants, >>, products] """
+    if len(text_list) != 3:
+        raise errors.TokenizeError("Invalid BigSMILES reaction. Too many or few '>' or '>>' detected. "
+                                   f"\nDetected chunks: {len(text_list)} (expected chunks: 3)")
+    if ">" in text_list:
+        raise errors.TokenizeError("Invalid BigSMILES reaction. Reactions must follow one of these patterns: "
+                                   "\n\t 'reactants >> products'\n\t 'reactants > agents > products' ")
+    if text_list[1] != ">>":
+        raise errors.TokenizeError("Invalid BigSMILES reaction. More than one '>>' detected.")
+
+    reactants = process_chemical_block(text_list[0])
+    products = process_chemical_block(text_list[2])
+
+    return reactants, [], products
+
+
+def parse_agent_reaction(text_list: list[str]) -> tuple[list[BigSMILES], list[BigSMILES], list[BigSMILES]]:
+    """ the pattern should be [reactants, >, agents, >, products] """
+    if len(text_list) != 5:
+        raise errors.TokenizeError("Invalid BigSMILES reaction. Too many or few '>' or '>>' detected. "
+                                   f"\nDetected chunks: {len(text_list)} (expected chunks: 5)")
+    if text_list[1] != ">" and text_list[3] != ">":
+        raise errors.TokenizeError("Invalid BigSMILES reaction. "
+                                   "The pattern should be: 'reactants > agents > products'.")
+
+    reactants = process_chemical_block(text_list[0])
+    agents = process_chemical_block(text_list[2])
+    products = process_chemical_block(text_list[4])
+
+    return reactants, agents, products
+
+
+def process_chemical_block(text: str) -> list[BigSMILES]:
+    text_list = comma_split(text)
+
+    chemicals = []
+    for chunk in text_list:
+        chemical = BigSMILES(chunk)
+        chemical = split_chemical(chemical)
+        chemicals += chemical
+
+    return chemicals
+
+
+def comma_split(text: str) -> list[str]:
+    """ splits on commas only at the top level (don't hit on commas in stochastic objects) """
+    flag = 0
+    buffer = ""
+    result = []
+    for char_ in text:
+        if char_ == "{":
+            flag += 1
+        elif char_ == "}":
+            flag -= 1
+        elif char_ == "," and flag == 0:
+            result.append(buffer)
+            buffer = ""
+            continue
+
+        buffer += char_
+
+    if buffer:
+        result.append(buffer)
+
+    return result
+
+
+## Split chemicals with '.' disconnect notation ## noqa
+#######################################################################################################################
+def split_chemical(bigsmiles_: BigSMILES) -> list[BigSMILES]:
+    """
+    Tries to split a BigSMILES at disconnects '.' if it thinks they are different molecules/polymers.
+
+    If it can't determine, it won't do anything.
+
+    Parameters
+    ----------
+    bigsmiles_: BigSMILES
+        bigsmiles which may or may not be made of multiple molecules/polymers seperated by disconnect notation '.'
+
+    Returns
+    -------
+    results: list[BigSMILES]
+        list of BigSMILES of individual molecules/polymers
+
+    """
+    if not bigsmiles_.has_disconnect:
+        return [bigsmiles_]
+
+    split_index = determine_split_index(bigsmiles_)
+    if not split_index:
+        return [bigsmiles_]
+
+    return split_bigsmiles(bigsmiles_, split_index, True)
+
+
+def determine_split_index(bigsmiles_: BigSMILES) -> list[int]:
+    disconnect_index = []
+    for i, node in enumerate(bigsmiles_.nodes):
+        if isinstance(node, Bond) and node.bond_order == 0:
+            disconnect_index.append(i)
+
+    split_index = []  # list[int]
+    buffer = 0
+    can_split = True
+    for index_ in disconnect_index:
+        buffer_fragment = 0
+        # check fragment
+        for node in bigsmiles_.nodes[:index_]:
+            if find_bridging_bond(node, bigsmiles_.nodes[index_].id_):
+                can_split = False
+                break
+
+            if isinstance(node, Atom) and node.charge != 0:
+                buffer_fragment += node.charge
+
+        buffer += buffer_fragment
+        if buffer == 0 and can_split:
+            split_index.append(index_)
+
+    return split_index
+
+
+def find_bridging_bond(node, cut_off_index: int) -> bool:
+    """
+    check bonds for an atom that has an id_ greater than cut off index.
+    cut off index is the disconnect symbol index so atom index is greater than the bond bridges across a disconnect
+    """
+    if not hasattr(node, "bonds"):
+        return False
+
+    for bond in node.bonds:
+        if bond.bond_order == 0:
+            continue
+        for atom in bond:
+            if atom.id_ > cut_off_index:
+                return True  # found a bond that bridge across disconnect
+
+    return False
+
+
+def split_bigsmiles(bigsmiles_: BigSMILES, split_index: list[int], delete_index: bool = False) -> list[BigSMILES]:
+    """ Brake bigsmiles up into multiple BigSMILES. """
+    if not split_index:
+        return [bigsmiles_]
+    split_index.sort()
+
+    result: list[BigSMILES] = []
+    for index_ in reversed(split_index):
+        new_bigsmiles = BigSMILES()
+
+        # delete bond
+        if delete_index:
+            bigsmiles_.nodes[index_].delete()
+
+        # move nodes
+        new_bigsmiles.nodes += bigsmiles_.nodes[index_:]
+        del bigsmiles_.nodes[index_:]
+
+        # move atoms
+        atom_index = get_index(new_bigsmiles.nodes, bigsmiles_.atoms, Atom)
+        if atom_index is not None:
+            new_bigsmiles.root.atoms += bigsmiles_.atoms[atom_index:]
+            del bigsmiles_.atoms[atom_index:]
+
+        # move bonds
+        bond_index = get_index(new_bigsmiles.nodes, bigsmiles_.bonds, Bond)
+        if bond_index is not None:
+            new_bigsmiles.root.bonds += bigsmiles_.bonds[bond_index:]
+            del bigsmiles_.bonds[bond_index:]
+
+        # move rings
+        ring_index = get_ring_index(new_bigsmiles, bigsmiles_)
+        if ring_index is not None:
+            new_bigsmiles.root.rings += bigsmiles_.rings[index_:]
+            del bigsmiles_.rings[index_:]
+
+        set_new_parent(new_bigsmiles, new_bigsmiles)
+        re_number_node_ids(new_bigsmiles)
+        result.append(new_bigsmiles)
+
+    result.append(bigsmiles_)
+    return list(reversed(result))
+
+
+def set_new_parent(new_parent, obj):
+    """ re-direct 'parent' to new bigsmiles object; only need to do the first layer """
+    for node in obj.nodes:
+        if hasattr(node, 'parent'):
+            node.parent = new_parent
+
+
+def get_index(node_list, other_list, type_) -> int | None:
+    for node in node_list:
+        if isinstance(node, type_):
+            break
+    else:
+        return None
+
+    for i, obj in enumerate(other_list):
+        if obj == node:
+            return i
+
+    return None
+
+
+def get_ring_index(new_bigsmiles: BigSMILES, old_bigsmiles: BigSMILES) -> int | None:
+    for i, ring in enumerate(old_bigsmiles.rings):
+        if ring.atom1 in new_bigsmiles.atoms:
+            return i
+
+    return None
+
+
+def re_number_node_ids(obj, id_: int = 0):
+    """ Recursive renumbering 'id_'. """
+    root = obj.root
+    for node in obj.nodes:
+        node.id_ = root._get_id(type(node))
+        if hasattr(node, 'nodes'):
+            re_number_node_ids(node, id_)
diff --git a/src/bigsmiles/constructors/constructor_str.py b/src/bigsmiles/constructors/constructor_str.py
new file mode 100644
index 0000000..2a35724
--- /dev/null
+++ b/src/bigsmiles/constructors/constructor_str.py
@@ -0,0 +1,266 @@
+"""
+
+Extend existing construction functions for string input
+
+"""
+from __future__ import annotations
+from functools import wraps
+
+from bigsmiles.errors import BigSMILESError
+from bigsmiles.constructors.tokenizer import tokenize_atom_symbol, tokenize_bonding_descriptor
+from bigsmiles.data_structures.bigsmiles import StochasticObject, StochasticFragment, BigSMILES, Branch
+from bigsmiles.constructors.constructor import *
+
+
+def in_stochastic_object(func):
+    """ Decorator to ensure function call only occurs within stochastic object."""
+
+    @wraps(func)
+    def _in_stochastic_object(*args, **kwargs):
+        if not args[0].in_stochastic_object:
+            raise BigSMILESError("Must in stochastic object.")
+        return func(*args, **kwargs)
+
+    return _in_stochastic_object
+
+
+## Extend existing construction functions for string input ##
+#######################################################################################################################
+def add_atom_str(parent: BigSMILES | Branch | StochasticObject | StochasticFragment, symbol: str) \
+        -> BigSMILES | Branch | StochasticObject | StochasticFragment:
+    """
+    appends an `Atom` to the end of the `parent`
+
+    Parameters
+    ----------
+    parent:
+        parent Atom will be added to
+    symbol:
+        atom symbol - will be parsed into symbol, isotope, charge, etc.
+
+    Returns
+    -------
+    parent:
+        returns same parent that was provided
+
+    """
+    return add_atom(parent, **tokenize_atom_symbol(symbol))
+
+
+@in_stochastic_object
+def add_bonding_descriptor_atom_str(parent: BigSMILES | Branch | StochasticObject | StochasticFragment,
+                                    bd_symbol: str, bond_symbol: str | None = None)\
+        -> BigSMILES | Branch | StochasticObject | StochasticFragment:
+    """
+    appends an `BondDescriptorAtom` to the end of the `parent`
+
+    Parameters
+    ----------
+    parent: BigSMILES, Branch, StochasticFragment
+        parent that the BondDescriptorAtom will be added to
+    bd_symbol: str
+        bonding descriptor symbol - will be parsed into symbol, index
+    bond_symbol: str
+        bond symbol ("", "=", "#", etc.)
+
+    Returns
+    -------
+    parent:
+        returns same parent that was provided
+
+    """
+    return add_bonding_descriptor_atom(parent, *tokenize_bonding_descriptor(bd_symbol), bond_symbol=bond_symbol)
+
+
+def add_bond_atom_pair_str(parent: BigSMILES | Branch | StochasticObject | StochasticFragment,
+                           bond_symbol: str | None, atom_symbol: str) \
+        -> BigSMILES | Branch | StochasticObject | StochasticFragment:
+    """
+     appends an `Bond` followed by an 'Atom' to the end of the `parent`
+
+    Parameters
+    ----------
+    parent: BigSMILES, Branch, StochasticFragment
+        parent that the Bond and Atom will be added to
+    bond_symbol: str
+        bond symbol ("", "=", "#", etc.)
+    atom_symbol: str
+        atom symbol - will be parsed into symbol, isotope, charge, etc.
+
+    Returns
+    -------
+    parent:
+        returns same parent that was provided
+
+    """
+    return add_bond_atom_pair(parent, bond_symbol, **tokenize_atom_symbol(atom_symbol))
+
+
+@in_stochastic_object
+def add_bond_bonding_descriptor_pair_str(parent: BigSMILES | Branch | StochasticObject | StochasticFragment,
+                                         bond_symbol: str | None, bd_symbol: str)\
+        -> BigSMILES | Branch | StochasticObject | StochasticFragment:
+    """
+    appends an `Bond` followed by an 'BondDescriptorAtom' to the end of the `parent`
+
+    Parameters
+    ----------
+    parent: BigSMILES, Branch, StochasticFragment
+        parent that the Bond and BondDescriptorAtom will be added to
+    bond_symbol: str
+        bond symbol ("", "=", "#", etc.)
+    bd_symbol: str
+        bonding descriptor symbol - will be parsed into symbol, index
+
+    Returns
+    -------
+    parent:
+        returns same parent that was provided
+
+    """
+    return add_bond_bonding_descriptor_pair(parent, bond_symbol, *tokenize_bonding_descriptor(bd_symbol))
+
+
+def open_stochastic_object_fragment_str(parent: BigSMILES | Branch | StochasticObject | StochasticFragment,
+                                        bd_symbol: str) -> StochasticFragment:
+    """
+    opens 'StochasticObject' followed by opening 'StochasticFragment'
+
+    Parameters
+    ----------
+    parent: BigSMILES, Branch, StochasticFragment
+    bd_symbol: str
+        bonding descriptor symbol - will be parsed into symbol, index
+
+    Returns
+    -------
+    parent: StochasticFragment
+        returns parent is stochastic fragment
+
+    """
+    return open_stochastic_object_fragment(parent, *tokenize_bonding_descriptor(bd_symbol))
+
+
+def open_stochastic_object_str(parent: BigSMILES | Branch | StochasticObject | StochasticFragment,
+                               bd_symbol: str) -> StochasticObject:
+    """
+    opens 'StochasticObject'
+
+    Parameters
+    ----------
+    parent: BigSMILES, Branch, StochasticFragment
+    bd_symbol: str
+        bonding descriptor symbol - will be parsed into symbol, index
+
+    Returns
+    -------
+    parent: StochasticFragment
+        returns parent is stochastic object
+
+    """
+    return open_stochastic_object(parent, *tokenize_bonding_descriptor(bd_symbol))
+
+
+def open_stochastic_object_fragment_with_bond_str(parent: BigSMILES | Branch | StochasticObject | StochasticFragment,
+                                                  bond_symbol: str | None, bd_symbol: str) \
+        -> StochasticFragment:
+    """
+    opens 'StochasticObject' followed by opening 'StochasticFragment'
+
+    Parameters
+    ----------
+    parent: BigSMILES, Branch, StochasticFragment
+        parent of StochasticObject
+    bd_symbol: str
+        bonding descriptor symbol - will be parsed into symbol, index
+    bond_symbol: str
+        bond symbol ("", "=", "#", etc.)
+
+    Returns
+    -------
+    parent: StochasticFragment
+        returns parent is StochasticFragment
+
+    """
+    return open_stochastic_object_fragment_with_bond(parent, bond_symbol, *tokenize_bonding_descriptor(bd_symbol))
+
+
+def close_stochastic_object_str(parent: BigSMILES | Branch | StochasticObject | StochasticFragment, bd_symbol: str,
+                                bond_symbol: str | None) \
+        -> BigSMILES | Branch | StochasticObject | StochasticFragment:
+    """
+    closes 'StochasticObject' and append it to `parent`
+
+    Parameters
+    ----------
+    parent: StochasticObject
+        parent of StochasticObject
+    bd_symbol: str
+        bonding descriptor symbol (right side) - will be parsed into symbol, index
+    bond_symbol: str
+        bond symbol ("", "=", "#", etc.)
+
+    Returns
+    -------
+    parent.parent: BigSMILES, Branch, StochasticFragment
+        returns parent of StochasticObject
+    """
+    return close_stochastic_object(parent, *tokenize_bonding_descriptor(bd_symbol), bond_symbol=bond_symbol)
+
+
+@in_stochastic_object
+def open_stochastic_fragment(parent) -> StochasticFragment:
+    """
+    opens 'StochasticFragment'
+
+    Parameters
+    ----------
+    parent: StochasticObject
+        parent of StochasticFragment
+
+    Returns
+    -------
+    parent: StochasticFragment
+        returns the freshly opened StochasticFragment
+
+    """
+    return open_stochastic_fragment(parent)
+
+
+@in_stochastic_object
+def close_open_stochastic_fragment_str(parent) -> StochasticFragment:
+    """
+    closes `StochasticFragment` and appends it to 'parent.parent' and opens a new `StochasticFragment`
+
+    Parameters
+    ----------
+    parent: StochasticFragment
+        StochasticFragment to be closed
+
+    Returns
+    -------
+    parent: StochasticFragment
+        returns the freshly opened StochasticFragment
+
+    """
+    return close_open_stochastic_fragment(parent)
+
+
+@in_stochastic_object
+def close_stochastic_fragment_str(parent: StochasticFragment) \
+        -> BigSMILES | Branch | StochasticObject | StochasticFragment:
+    """
+    closes `StochasticFragment` and appends it to 'parent.parent'
+
+    Parameters
+    ----------
+    parent: StochasticFragment
+        StochasticFragment to be closed
+
+    Returns
+    -------
+    parent.parent: StochasticObject
+        returns the parent.parent
+
+    """
+    return close_stochastic_fragment(parent)
diff --git a/src/bigsmiles/constructors/constructor_tokens.py b/src/bigsmiles/constructors/constructor_tokens.py
new file mode 100644
index 0000000..c900460
--- /dev/null
+++ b/src/bigsmiles/constructors/constructor_tokens.py
@@ -0,0 +1,267 @@
+from __future__ import annotations
+
+import logging
+
+import bigsmiles.errors as errors
+from bigsmiles.validation.validation_string import run_string_validation
+from bigsmiles.constructors.tokenizer import Token, TokenKind, tokenize
+from bigsmiles.validation.validation_tokens import run_token_validation
+import bigsmiles.constructors.constructor_str as constructor
+from bigsmiles.data_structures.bigsmiles import BigSMILES, Branch, StochasticFragment, Atom, StochasticObject, \
+    has_node_attr, BondDescriptorAtom
+
+
+def get_next_token(tokens: list[Token], text: str = "") -> Token:
+    try:
+        return tokens.pop(0)
+    except IndexError:
+        raise errors.BigSMILESError(f"{text} can't be at the end of a BigSMILES string.")
+
+
+atom_tokens = {TokenKind.Atom, TokenKind.AtomExtend, TokenKind.Aromatic}
+aromatic_acceptors = (Atom, StochasticObject, BondDescriptorAtom)
+map_next_token_to_bond = {
+    TokenKind.Aromatic: ":",
+    TokenKind.Atom: "",
+    TokenKind.AtomExtend: "",
+    TokenKind.StochasticStart: "",
+    TokenKind.Disconnected: ".",
+    TokenKind.BondDescriptor: "",
+}
+
+
+## Functions for mapping tokens to BigSMILES ##
+#######################################################################################################################
+def map_atom(parent: has_node_attr, tokens: list[Token], token: Token):
+    if isinstance(parent, BigSMILES) and not parent:
+        return constructor.add_atom_str(parent, token.value)
+
+    if isinstance(parent, StochasticFragment) and len(parent.nodes) == 0:
+        return constructor.add_atom_str(parent, token.value)
+
+    if token.kind is TokenKind.Aromatic and constructor.get_prior(parent, aromatic_acceptors).aromatic:
+        return constructor.add_bond_atom_pair_str(parent, ":", token.value)
+
+    return constructor.add_bond_atom_pair_str(parent, "", token.value)
+
+
+def map_bond(parent: has_node_attr, tokens: list[Token], token: Token):
+    next_token = get_next_token(tokens, "Bond")
+
+    if next_token.kind in atom_tokens:
+        return constructor.add_bond_atom_pair_str(parent, token.value, next_token.value)
+
+    if next_token.kind is TokenKind.BondDescriptor:
+        return constructor.add_bond_bonding_descriptor_pair_str(parent, token.value, next_token.value)
+
+    if next_token.kind is TokenKind.StochasticStart:
+        return map_stochastic_object_start(parent, tokens, next_token, token)
+
+    if next_token.kind is TokenKind.Ring or next_token.kind is TokenKind.Ring2:
+        return constructor.add_ring_by_index(parent, int(next_token.value.replace('%', '')), token.value)
+
+    raise errors.BigSMILESError(f"Bond can't be followed by: {next_token.kind}")
+
+
+def map_bond_descriptor(parent: has_node_attr, tokens: list[Token], token: Token):
+    next_token = get_next_token(tokens, "Bond Descriptor")
+    if next_token.kind is TokenKind.StochasticEnd:
+        return map_stochastic_object_end(parent, tokens, token)
+
+    if isinstance(parent, Branch) and next_token.kind != TokenKind.BranchEnd:
+        # check to make sure the branch closes immediately
+        raise errors.BigSMILESError("If Bonding Descriptors is the first Branch symbol, "
+                                    "it can only be followed by Branch End.")
+
+    if isinstance(parent, StochasticFragment) and len(parent.nodes) == 0:
+        # first StochasticFragment symbol
+        # get bond
+        if next_token.kind is TokenKind.Bond:
+            bond = next_token.value
+        elif next_token.kind in map_next_token_to_bond:
+            bond = map_next_token_to_bond[next_token.kind]
+        elif next_token.kind in (TokenKind.Ring, TokenKind.Ring2):
+            bond = None
+        else:
+            raise errors.BigSMILESError(f"Invalid Symbol following stochastic object close.")
+        tokens.insert(0, next_token)
+
+        return constructor.add_bonding_descriptor_atom_str(parent, token.value, bond)
+
+    tokens.insert(0, next_token)
+    if constructor.get_prior(parent, (Atom, StochasticObject, BondDescriptorAtom)).aromatic:
+        bond = ":"
+    else:
+        bond = ""
+
+    return constructor.add_bond_bonding_descriptor_pair_str(parent, bond, token.value)
+
+
+def map_branch_start(parent: has_node_attr, tokens: list[Token], token: Token):
+    if isinstance(parent, Branch) and len(parent.nodes) == 0:
+        raise errors.BigSMILESError("Branch can't be the first object within a branch.")
+    return constructor.open_branch(parent)
+
+
+def map_branch_end(parent: has_node_attr, tokens: list[Token], token: Token):
+    return constructor.close_branch(parent)
+
+
+def map_ring(parent: has_node_attr, tokens: list[Token], token: Token):
+    if constructor.get_prior(parent, (Atom, StochasticObject)).aromatic:
+        bond = ":"
+    else:
+        bond = ""
+
+    return constructor.add_ring_by_index(parent, int(token.value.replace('%', '')), bond)
+
+
+def map_stochastic_object_start(parent: has_node_attr, tokens: list[Token], token: Token, bond_token: Token = None):
+    next_token = get_next_token(tokens, "Stochastic objects start symbol")
+
+    if next_token.kind is TokenKind.ImplictEndGroup:
+        return constructor.open_stochastic_object_fragment_str(parent, next_token.value)
+
+    if next_token.kind is not TokenKind.BondDescriptor:
+        raise errors.BigSMILESError(f"Stochastic object starts must be followed an explict or implicit end group.")
+
+    if isinstance(parent, BigSMILES) and not parent:
+        parent = constructor.add_atom_str(parent, "[H]")
+
+    if constructor.get_prior(parent, (Atom, StochasticObject)).aromatic:
+        bond = ":"
+    elif bond_token is None:
+        bond = ""
+    else:
+        bond = bond_token.value
+
+    return constructor.open_stochastic_object_fragment_with_bond_str(parent, bond, next_token.value)
+
+
+def map_stochastic_object_end(parent: has_node_attr, tokens: list[Token], token: Token) -> has_node_attr:
+    parent = constructor.close_stochastic_fragment_str(parent)
+
+    # get bond
+    if len(tokens) < 1:
+        bond = None
+    elif tokens[0].kind is TokenKind.Bond:
+        bond = tokens[0].value
+    elif tokens[0].kind in map_next_token_to_bond:
+        bond = map_next_token_to_bond[tokens[0].kind]
+    elif tokens[0].kind in (TokenKind.Ring, TokenKind.Ring2):
+        bond = None
+    else:
+        raise errors.BigSMILESError(f"Invalid Symbol following stochastic object close.")
+
+    return constructor.close_stochastic_object_str(parent, token.value, bond)
+
+
+def map_bond_seperator(parent: has_node_attr, tokens: list[Token], token: Token) -> has_node_attr:
+    return constructor.close_open_stochastic_fragment_str(parent)
+
+
+def map_reaction(parent: has_node_attr, tokens: list[Token], token: Token):
+    raise errors.BigSMILESError("Reaction Symbol detected. Use 'bigsmiles.Reaction()' to process this input.")
+
+
+def map_disconnect(parent: has_node_attr, tokens: list[Token], token: Token):
+    next_token = get_next_token(tokens, "Disconnect")
+    if next_token.kind in atom_tokens:
+        return constructor.add_bond_atom_pair_str(parent, ".", next_token.value)
+
+    if next_token.kind is TokenKind.StochasticStart:
+        return constructor.open_stochastic_object_fragment_with_bond_str(parent, ".", next_token.value)
+
+    if next_token.kind is TokenKind.BondDescriptor:
+        return constructor.add_bond_bonding_descriptor_pair_str(parent, ".", next_token.value)
+
+    raise errors.BigSMILESError(f"Disconnect can't be followed by '{next_token.kind.name}'. ")
+
+
+# def map_bondez():
+#     # the pattern must be Atom() / Atom () = Atom () / Atom
+
+
+def NotImplementedFunc(*args, **kwargs):
+    raise NotImplementedError()
+
+
+def SkipSymbol(parent: has_node_attr, tokens: list[Token], token: Token) -> has_node_attr:
+    logging.warning(f"Symbol skipped: {token.value}")
+    return parent
+
+
+# this dict maps a token kind to a function that will process or map the symbol into a BigSMILES
+map_token_functions = {
+    TokenKind.Bond: map_bond,
+    TokenKind.Atom: map_atom,
+    TokenKind.Aromatic: map_atom,
+    TokenKind.AtomExtend: map_atom,
+    TokenKind.BranchStart: map_branch_start,
+    TokenKind.BranchEnd: map_branch_end,
+    TokenKind.Ring: map_ring,
+    TokenKind.Ring2: map_ring,
+    TokenKind.BondEZ: map_bond,
+    TokenKind.Disconnected: map_disconnect,
+    TokenKind.Rxn: map_reaction,
+    TokenKind.BondDescriptor: map_bond_descriptor,
+    TokenKind.StochasticSeperator: map_bond_seperator,
+    TokenKind.StochasticStart: map_stochastic_object_start,
+    TokenKind.StochasticEnd: map_stochastic_object_end,
+    TokenKind.ImplictEndGroup: map_bond_descriptor,
+    TokenKind.BondDescriptorLadder: NotImplementedFunc
+}
+
+valid_first_symbols = {TokenKind.Atom, TokenKind.AtomExtend, TokenKind.Aromatic, TokenKind.StochasticStart}
+
+
+def tokens_to_bigsmiles(parent: has_node_attr, tokens: list[Token]):
+    """
+    Main loop for converting tokens into BigSMILES objects.
+
+    Parameters
+    ----------
+    parent:
+    tokens:
+        BigSMILES tokens
+
+    """
+    num_tokens = len(tokens)
+
+    # guard statements
+    if num_tokens < 1:
+        raise errors.BigSMILESError(f"No BigSMILES symbols detected.")
+    if tokens[0].kind not in valid_first_symbols:
+        raise errors.BigSMILESError(f"BigSMILES can't start with a '{tokens[0].kind.name}' symbol.")
+
+    # main loop
+    while tokens:
+        token = tokens.pop(0)
+        func = map_token_functions[token.kind]
+        try:
+            parent = func(parent, tokens, token)
+        except errors.BigSMILESError as e:
+            raise errors.BigSMILESError(f"Issue with token '{token}'. (token: {num_tokens - len(tokens) - 1})",
+                                        e) from e
+
+
+def parse_bigsmiles_str(input_text: str, bigsmiles: BigSMILES):
+    """
+    Main function that turns BigSMILES string tokens then into a BigSMILES object.
+
+    Parameters
+    ----------
+    input_text:
+        BigSMILES string
+    bigsmiles: BigSMILES
+        this BigSMILES will be built up
+
+    """
+    try:
+        input_text = run_string_validation(input_text)
+        tokens = tokenize(input_text)
+        tokens = run_token_validation(tokens)
+        tokens_to_bigsmiles(bigsmiles, tokens)
+        constructor.exit_construction(bigsmiles)
+    except errors.BigSMILESError as e:
+        raise errors.BigSMILESError(f"Parsing failed on '{input_text}'.", e) from e
diff --git a/src/bigsmiles/constructors/tokenizer.py b/src/bigsmiles/constructors/tokenizer.py
new file mode 100644
index 0000000..c023ffc
--- /dev/null
+++ b/src/bigsmiles/constructors/tokenizer.py
@@ -0,0 +1,295 @@
+"""
+
+Code for tokenizing a BigSMILES string.
+
+"""
+from __future__ import annotations
+import enum
+import re
+
+from bigsmiles.errors import TokenizeError
+import bigsmiles.reference_data.chemical_data as chemical_data
+
+
+class TokenKind(enum.Enum):
+    """
+    **Enum**
+
+    the kind of tokens that will be extracted from BigSMILES string
+
+    Attributes
+    ----------
+    Bond
+    Atom
+    Aromatic
+    AtomExtend
+    BranchStart
+    BranchEnd
+    Ring
+    Ring2
+    BondEZ
+    Disconnected
+    Rxn
+    BondDescriptor
+    StochasticSeperator
+    StochasticStart
+    StochasticEnd
+    ImplictEndGroup
+    BondDescriptorLadder
+
+    """
+    Bond = 0
+    Atom = 1
+    Aromatic = 2
+    AtomExtend = 3
+    BranchStart = 4
+    BranchEnd = 5
+    Ring = 6
+    Ring2 = 7
+    BondEZ = 8
+    Disconnected = 9
+    Rxn = 10
+    BondDescriptor = 11
+    StochasticSeperator = 12
+    StochasticStart = 13
+    StochasticEnd = 14
+    ImplictEndGroup = 15
+    BondDescriptorLadder = 16
+
+
+_isotope_pattern = r'(?P<isotope>[\d]{1,3})?'
+_element_pattern = r'(?P<symbol>' + "|".join(chemical_data.aromatic_elements) + "|" \
+                   + "|".join(chemical_data.elements_ordered) + ')'
+_stereo_pattern = r'(?P<stereo>@{1,2})?'
+_hydrogen_pattern = r'(?P<hydrogens>H[\d]?)?'
+_charge_pattern = r'(?P<charge>[-|\+]{1,3}[\d]?)?'
+_class_pattern = r'(?P<class_>:\d{1,3})?'
+atom_pattern = r"(?:\[)" + _isotope_pattern + _element_pattern + _stereo_pattern + \
+               _hydrogen_pattern + _charge_pattern + _class_pattern + r"(?:\])"
+
+token_specification = [
+    # order in the list is important; regex stops at first match
+    (TokenKind.Bond.name, r'[-|=|#|$]'),
+    (TokenKind.Atom.name, "|".join(chemical_data.organic_ordered)),
+    (TokenKind.Aromatic.name, "|".join(chemical_data.aromatic_elements)),
+    (TokenKind.AtomExtend.name, atom_pattern),  # Atom in brackets
+    (TokenKind.BranchStart.name, r'\('),
+    (TokenKind.BranchEnd.name, r'\)'),
+    (TokenKind.Ring.name, r'[\d]{1}'),
+    (TokenKind.Ring2.name, r'%[\d]{2}'),  # ring_id with two-digit numbers
+    (TokenKind.BondEZ.name, r'/|\\'),  # cis trans
+    (TokenKind.Disconnected.name, r"\."),  # mixture
+
+    (TokenKind.BondDescriptorLadder.name, r"\[[$<>][\d]\[[$<>][\d]?\][\d]?\]"),  # Ladder
+    (TokenKind.BondDescriptor.name, r"\[[$<>][\d]?[\d]?\]"),
+    (TokenKind.StochasticSeperator.name, r",|;"),
+    (TokenKind.StochasticStart.name, r'\{'),
+    (TokenKind.StochasticEnd.name, r'\}'),
+    (TokenKind.ImplictEndGroup.name, r'\[\]'),
+
+    (TokenKind.Rxn.name, r">>|>"),  # reaction -->
+
+    ('SKIP', r'[ \t]+'),  # Skip over spaces and tabs
+    ('MISMATCH', r'.'),  # Any other character
+]
+
+tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
+
+tok_regex = re.compile(tok_regex)
+
+
+class Token:
+    """
+    token; what a BigSMILES string gets broken up into
+    """
+    __slots__ = ("kind", "value")
+
+    def __init__(self, kind: TokenKind, value: str):
+        """
+        Parameters
+        ----------
+        kind: TokenKind
+            token kind
+        value: str
+            token text
+
+        Examples
+        --------
+        >>> Token(TokenKind.AtomExtend, "[13CH2]")
+
+        """
+        self.kind = kind
+        self.value = value
+
+    def __str__(self):
+        return f"{self.kind.name}: {self.value}"
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __eq__(self, other):
+        if self.kind != other.kind:
+            return False
+        if self.value != other.value:
+            return False
+        return True
+
+
+def tokenize(text: str) -> list[Token]:
+    """
+
+    tokenizes a bigSMILES string
+
+    Parameters
+    ----------
+    text: str
+        BigSMILES string
+
+    Returns
+    -------
+    result: list[Token]
+        BigSMILES as a token list
+
+    Raises
+    ------
+    TokenizeError
+        invalid symbol detected
+
+    Examples
+    --------
+    >>> tokenize("C(C)C")
+    [Token(TokenKind.Atom, "C"), Token(TokenKind.BranchStart, "("), Token(TokenKind.Atom, "C"),
+    Token(TokenKind.BranchEnd, ")"), Token(TokenKind.Atom, "C")]
+
+    """
+    result = []
+    for match in re.finditer(tok_regex, text.replace(" ", "")):
+        kind = match.lastgroup
+
+        value = match.group()
+        if kind == 'SKIP':
+            continue
+        elif kind == 'MISMATCH':
+            if text[:2] in chemical_data.element_symbols:
+                raise TokenizeError(f"Invalid symbol. If the symbol is not in the list below it must be in []."
+                                    f"\nNon-bracket elements: {chemical_data.organic_elements}"
+                                    f"\n(starting with {value!r}; index: {match.span()[0]})"
+                                f'\n{text}' + "\n" + " " * match.span()[0] + "^(and forward)")
+
+            raise TokenizeError(f'Invalid symbol (or group of symbols). (starting with {value!r}; '
+                                f'index: {match.span()[0]})'
+                                f'\n{text}' + "\n" + " " * match.span()[0] + "^(and forward)")
+
+        result.append(
+            Token(TokenKind[kind], value)
+        )
+
+    return result
+
+
+ATOM_PATTERN = re.compile(atom_pattern)
+
+
+def tokenize_atom_symbol(symbol: str) -> dict:
+    """
+    convert atom symbol into a dictionary of the following values:
+
+    * symbol
+    * isotope (default: None)
+    * stereo  (default: None)
+    * hydrogens  (default: None)
+    * charge  (default: 0)
+    * class_  (default: None)
+
+    Parameters
+    ----------
+    symbol: str
+        atom string
+
+    Returns
+    -------
+    result: dict[str: str | int]
+        {"symbol": str, "isotope": int, "stereo": str, "hydrogens": int, "charge": int, "class_": int}
+
+    Examples
+    --------
+    >>> tokenize_atom_symbol("[13C@H+:1]")
+    {"symbol": "C", "isotope": 13, "stereo": "@", "hydrogens": 1, "charge": 1, "class_": 1}
+
+    """
+    if symbol in chemical_data.elements_aromatic:
+        return {"symbol": symbol, "isotope": None, "stereo": None, "hydrogens": None, "charge": 0, "class_": None}
+
+    try:
+        results = ATOM_PATTERN.match(symbol).groupdict()
+
+        if results["isotope"] is not None:
+            results["isotope"] = int(results["isotope"])
+
+        if results["hydrogens"] is None:
+            results["hydrogens"] = 0
+        else:
+            if results["hydrogens"][-1].isdigit():
+                results["hydrogens"] = int(results["hydrogens"][-1])
+            else:
+                results["hydrogens"] = 1
+
+        if results["charge"] is None:
+            results["charge"] = 0
+        elif results["charge"] == "+":
+            results["charge"] = 1
+        elif results["charge"] == "-":
+            results["charge"] = -1
+        elif results["charge"].count("+") > 1:
+            results["charge"] = results["charge"].count("+")
+        elif results["charge"].count("-") > 1:
+            results["charge"] = -1 * results["charge"].count("-")
+        else:
+            results["charge"] = int(results["charge"].replace("+", ""))
+
+        if results["class_"] is not None:
+            results["class_"] = int(results["class_"][1:])  # index 0 is ":", so skip it
+
+        return results
+    except AttributeError:
+        raise TokenizeError(f"Issue tokenizing atom: {symbol}")
+
+
+DEFAULT_BONDING_DESCRIPTOR_INDEX = 1
+
+
+def tokenize_bonding_descriptor(symbol: str) -> tuple[str, int]:
+    """
+    convert bonding descriptor symbol into the following values:
+
+    * symbol
+    * index
+
+    Parameters
+    ----------
+    symbol: str
+        bonding_descriptor string
+
+    Returns
+    -------
+    result: tuple[str, int]
+        [symbol, index]
+
+    Examples
+    --------
+    >>> tokenize_atom_symbol("[$1]")
+    ["$", 1]
+
+    Notes
+    -----
+    * default bonding descriptor index = 1
+
+    """
+    symbol = symbol.replace("[", "").replace("]", "")
+    if not symbol:
+        return symbol, DEFAULT_BONDING_DESCRIPTOR_INDEX
+
+    if symbol[-1].isdigit():
+        return symbol[0], int(symbol[1:])
+
+    return symbol, DEFAULT_BONDING_DESCRIPTOR_INDEX
diff --git a/src/bigsmiles/data_structures/__init__.py b/src/bigsmiles/data_structures/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bigsmiles/data_structures/bigsmiles.py b/src/bigsmiles/data_structures/bigsmiles.py
new file mode 100644
index 0000000..b6cd4ff
--- /dev/null
+++ b/src/bigsmiles/data_structures/bigsmiles.py
@@ -0,0 +1,1229 @@
+"""
+
+The following contains the following data structures:
+* Atom
+* Bond
+* Branch
+* Bond descriptor
+* Bond descriptor atom
+* Stochastic fragment
+* Stochastic object
+* BigSMILES
+
+"""
+
+from __future__ import annotations
+import logging
+
+import bigsmiles.errors as errors
+import bigsmiles.reference_data.chemical_data as chemical_data
+from bigsmiles.config import Config
+
+_conjugated_warning = True
+
+
+class Atom:
+    """
+    this class represents an atom
+    """
+    __slots__ = ["id_", "symbol", "isotope", "stereo", "hydrogens", "charge", "class_", "organic",
+                 "aromatic", "valence", "possible_valence", "_default_valence", "_valene_warning_raised", "_bonds",
+                 "parent", "__dict__"]
+    _eq_attr = ("id_", "symbol", "isotope", "stereo", "hydrogens", "charge", "valence", "aromatic")
+
+    def __init__(self,
+                 id_: int,
+                 symbol: str,
+                 isotope: int | None = None,
+                 stereo: str | None = None,
+                 hydrogens: int | None = None,
+                 charge: int = 0,
+                 valence: int | float | None = None,
+                 class_: int | None = None,
+                 parent: BigSMILES | Branch | StochasticFragment | None = None,
+                 **kwargs
+                 ):
+        """
+        Attributes
+        ----------
+        id_: int
+            id of atom (id is limited to atoms). Range: [1, inf]
+        symbol: str
+            symbol symbol (e.g., H, C, O, Zn)
+        isotope: int | None
+            isotope (e.g., [13C])
+        stereo: str | None
+            stereochemistry [None, "@", "@@"] (None means not defined)
+        hydrogens: int | None
+            number of explict hydrogens  (e.g., [CH2])
+        charge: int
+            symbol charge  (e.g., [Fe+3])
+        valence: int
+            The capacity to form bonds with other atoms
+        class_: int | None
+            index of class (e.g., [C:1] class_ = 1)
+        parent: BigSMILES | Branch | StochasticFragment | None
+            the owner of the atom
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.id_ = id_
+        self.symbol = symbol.capitalize()
+        self.isotope = isotope
+        self.stereo = stereo
+        self.hydrogens = hydrogens
+        self.charge = charge
+        self.class_ = class_
+        self.organic = True if self.symbol in chemical_data.organic_elements else False
+        # TODO: calculate aromatic
+        self.aromatic = True if symbol in chemical_data.aromatic_elements else False
+        self.possible_valence: tuple[int] = chemical_data.atom_valences[self.symbol]
+        if valence is None:
+            self._default_valence: bool = True
+            self.valence = self.possible_valence[0]
+        else:
+            self._default_valence: bool = False
+            self.valence = valence
+
+        self.parent = parent
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+        self._bonds = []
+        self._valene_warning_raised = False
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(Atom,))
+
+    def __eq__(self, other: Atom):
+        """
+        equality is based on the following parameters:
+
+        * id_
+        * symbol
+        * isotope
+        * stereo
+        * hydrogens
+        * charge
+        * valence
+        * aromatic
+        * bonds ~ bond.id_
+        """
+        if not isinstance(other, Atom):
+            return False
+
+        # check attributes
+        for attr in self._eq_attr:
+            if getattr(self, attr) != getattr(other, attr):
+                return False
+
+        # check atoms
+        for bond, other_bond in zip(self.bonds, other.bonds):
+            if bond.id_ != other_bond.id_:
+                return False
+
+        return True
+
+    # def __hash__(self):
+    #     NotImplemented and shouldn't according to Python docs because bond is mutable. If the Atom is hashed, a bond
+    #     add; then the hash would be wrong. Instead, use '.id_' for this purpose.
+    #     Docs: https://docs.python.org/3/reference/datamodel.html#object.__hash__
+    #
+
+    @property
+    def bonds(self) -> list[Bond]:
+        """ list of bonds (excludes implicit hydrogens) """
+        return self._bonds
+
+    @bonds.setter
+    def bonds(self, bonds: list[Bond]):
+        self._bonds = bonds
+        # Validation for available valence
+        if self.number_of_bonds > self.bond_capacity:
+            if not self._increase_valence(self.number_of_bonds - self.bond_capacity):
+                logging.error(f"Too many bonds trying to be made. {str(self)}")
+
+    def delete_bond(self, delete_bond: Bond | list[Bond]):
+        """
+        delete bond or bonds
+
+        Parameters
+        ----------
+        delete_bond: Bond | list[Bond]
+
+        """
+        if not isinstance(delete_bond, list):
+            delete_bond = [delete_bond]
+
+        for bond in delete_bond:
+            try:
+                self.bonds.remove(bond)
+            except ValueError:
+                raise ValueError("Bond not connect to atom so it can't be deleted."
+                                 f"\n Atom:{self.details}\nBond attempting to remove: {bond.details}")
+
+    @property
+    def implicit_hydrogens(self) -> float:
+        """ number of implicit hydrogens. (zero if explict hydrogens have been specified) """
+        return self.bonds_available if self.hydrogens is None else 0
+
+    @property
+    def bond_capacity(self) -> float:
+        """ total capacity of the atom account for charge """
+        return self.valence + self.charge
+
+    @property
+    def number_of_bonds(self) -> float:
+        """ number of bonds already formed (not including implicit hydrogens; but including explicit hydrogens) """
+        num_bonds = sum((bond.bond_order for bond in self.bonds))
+        if self.hydrogens is not None:
+            num_bonds += self.hydrogens
+        return num_bonds
+
+    @property
+    def bonds_available(self) -> float:
+        """ number bonds that remain open for bonding """
+        bonds_available = self.bond_capacity - self.number_of_bonds
+        if bonds_available < 0:
+            return 0
+        return bonds_available
+
+    @property
+    def full_valence(self) -> bool:
+        """ returns true if the atom valence is full """
+        if self.valence - self.number_of_bonds - self.implicit_hydrogens - abs(self.charge) == 0:
+            return True
+
+        return False
+
+    @property
+    def ring_indexes(self) -> list[int]:
+        """ list of ring index """
+        ring_index = []
+        for bond in self.bonds:
+            if bond.ring_id is not None:
+                ring_index.append(bond.ring_id)
+
+        return ring_index
+
+    @property
+    def details(self) -> str:
+        """ long string representation """
+        text = str(self) + "  {"
+        for attr in self._eq_attr:
+            text += f"{attr}: {getattr(self, attr)}, "
+        return text[:-2] + "}"
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        text = self._to_string(show_hydrogens, show_atom_index, show_repr)
+        if not self.full_valence and not self._valene_warning_raised:
+            self._valene_warning_raised = True
+            logging.warning(f"Incomplete valence detected on atom: {text}")
+
+        return text
+
+    def _to_string(self, show_hydrogens: bool = False, show_atom_index: bool = True, show_repr: tuple | None = None)\
+            -> str:
+        text = ""
+        bracket_flag = False
+
+        if self.symbol == "H":
+            bracket_flag = True
+
+        if self.isotope is not None:
+            text += str(self.isotope)
+            bracket_flag = True
+
+        if self.aromatic:
+            text += self.symbol.lower()
+        else:
+            text += self.symbol
+
+        if self.stereo:
+            text += self.stereo
+            bracket_flag = True
+
+        if show_hydrogens or self.hydrogens is not None:
+            if self.hydrogens is not None:
+                if self.hydrogens == 1:
+                    text += f"H"
+                elif self.hydrogens > 1:
+                    text += f"H{self.hydrogens}"
+
+                bracket_flag = True
+            else:
+                if self.implicit_hydrogens == 1:
+                    text += f"H"
+                    bracket_flag = True
+                if self.implicit_hydrogens > 1:
+                    text += f"H{self.implicit_hydrogens}"
+                    bracket_flag = True
+
+        if self.charge > 0:
+            text += f"+{self.charge if self.charge > 1 else ''}"
+            bracket_flag = True
+        elif self.charge < 0:
+            text += f"-{self.charge if self.charge > 1 else ''}"
+            bracket_flag = True
+
+        if self.class_ is not None and show_atom_index:
+            text += ":" + str(self.class_)
+
+        if bracket_flag:
+            text = "[" + text + "]"
+
+        if self.ring_indexes:
+            # get ring ids
+            rings = []
+            for bond in self.bonds:
+                if bond.ring_id is not None:
+                    rings.append([bond.ring_id, bond])
+
+            # sort to numerical order
+            rings.sort(key=lambda x: x[0])
+            for ring_id, bond in rings:
+                ring_text = ""
+                if bond.symbol != ":":  # don't show aromatic bonds in ring index
+                    if not self.root.contains_stochastic_object and not Config.show_multi_bonds_on_both_ring_index \
+                            and bond.atom2 == self:
+                        pass
+                    else:
+                        ring_text += bond.symbol
+                if ring_id > 9:
+                    ring_text += "%"
+
+                text += ring_text + str(ring_id)
+
+        if show_repr is not None and Atom in show_repr:
+            text += "{" + str(self.id_) + "}"
+
+        return text
+
+    def _increase_valence(self, requested_valence_increase: float = 0) -> bool:
+        """ If default valence, try to increase when an additional bond added that needs it. And does the increase!! """
+        if not self._default_valence:
+            return False
+
+        for valence in self.possible_valence:
+            if valence > self.valence:
+                old_valence = self.valence
+                self.valence = valence
+                if self.bonds_available < requested_valence_increase:
+                    # increase was not enough for requested valence increase; set valence back to original value
+                    self.valence = old_valence
+                    continue
+
+                return True
+
+        return False
+
+    @property
+    def root(self) -> BigSMILES:
+        """ the owner at the top of the parent tree """
+        return self.parent.root
+
+
+class Bond:
+    """
+    this class represents a bond
+    """
+    __slots__ = ["id_", "symbol", "atom1", "atom2", "ring_id", "parent", "__dict__", "double_bond_stereo"]
+    _eq_attr = ("id_", "symbol", "ring_id") # "double_bond_ez"
+
+    def __init__(self,
+                 id_: int,
+                 symbol: str,
+                 atom1: Atom | BondDescriptorAtom | StochasticObject,
+                 atom2: Atom | BondDescriptorAtom | StochasticObject | None = None,
+                 ring_id: int | None = None,
+                 parent: BigSMILES | Branch | StochasticFragment | None = None,
+                 **kwargs
+                 ):
+        """
+
+        Parameters
+        ----------
+        id_: int
+            id of bond (id is limited to bonds). Range: [1, inf]
+        symbol: str
+            bond symbol (e.g. '', '=', '#')
+        atom1: Atom | BondDescriptorAtom | StochasticObject
+            atom that the bond starts at
+        atom2: Atom | BondDescriptorAtom | StochasticObject | None
+            atom that the bond ends at
+        ring_id: int | None
+            if the bond is closing a ring, this is the index for that ring
+        parent: BigSMILES | Branch | StochasticFragment | None
+            the owner of the bond
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.id_ = id_
+        self.symbol = symbol
+        self.atom1 = atom1
+        self.atom2 = atom2
+        self.ring_id = ring_id
+        self.parent = parent
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(Bond,), skip_color=True)
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        text = ""
+        if self.symbol != ":" or (self.symbol == ":" and Config.show_aromatic_bond):
+            text += Config.add_color(self.symbol, 'Blue', skip_color)
+
+        if show_repr is not None and Bond in show_repr:
+            text += "{" + str(self.id_) + "}"
+
+        return text
+
+    def __iter__(self):
+        """ iterates over Atoms """
+        return iter((self.atom1, self.atom2))
+
+    def __reversed__(self):
+        return iter((self.atom2, self.atom1))
+
+    def __eq__(self, other: Bond):
+        """
+        equality is based on the following parameters:
+
+        * id_
+        * symbol
+        * ring_id
+        * atom1.id_
+        * atom2.id_
+         """
+        if not isinstance(other, Bond):
+            return False
+
+        # check attributes
+        for attr in self._eq_attr:
+            if getattr(self, attr) != getattr(other, attr):
+                return False
+
+        # check atoms
+        if self.atom1.id_ != other.atom1.id_ or self.atom2.id_ != other.atom2.id_:
+            return False
+
+        return True
+
+    def delete(self):
+        """ deletes bond and removes it from atoms and root """
+        self.atom1.delete_bond(self)
+        self.atom2.delete_bond(self)
+        self.parent.nodes.remove(self)
+        if self.parent is not self.root:
+            self.root.bonds.remove(self)
+        del self
+
+    @property
+    def details(self) -> str:
+        """ long string representation """
+        text = str(self) + "  {"
+        for attr in self._eq_attr:
+            text += f"{attr}: {getattr(self, attr)}, "
+        text += f"bond_order: {self.bond_order}, "
+        text += "atoms: " + repr(self.atom1) + " <--> " + repr(self.atom2)
+        return text + "}"
+
+    @property
+    def bond_order(self) -> int:
+        """  number of covalent bonds represented """
+        return chemical_data.bond_mapping[self.symbol]
+
+    @bond_order.setter
+    def bond_order(self, count: int):
+        for k, v in chemical_data.bond_mapping.items():
+            if v == count:
+                self.symbol = k
+                return
+
+        raise ValueError(f"Invalid bond_order. \nGiven: {count} \n Acceptable: {chemical_data.bond_mapping}")
+
+    @property
+    def root(self) -> BigSMILES:
+        """ the owner at the top of the parent tree """
+        return self.parent.root
+
+    # @property
+    # def double_bond_ez(self) -> str | None:
+    #     """
+    #     returns labels for geometric isomers of alkenes ["E", "Z", None]
+    #
+    #     ??? info "E/Z more information"
+    #
+    #         ::: bigsmiles.data_structures.stereo_rules.get_double_bond_ez
+    #
+    #     """
+    #     if self.symbol != "=":
+    #         return None
+    #
+    #     from bigsmiles.data_structures.stereo_rules import get_double_bond_ez
+    #     return get_double_bond_ez(self)
+
+    @property
+    def aromatic(self) -> bool:
+        """ Limited accuracy """
+        if self.symbol == ":":
+            return True
+        return False
+
+    # @property
+    # def conjugated(self) -> bool:
+    #     # TODO: small molecules and bridge across stochastic object and stochastic fragments
+    #     # only for
+    #
+    #     global _conjugated_warning  # so it is only raised once
+    #     if _conjugated_warning and self.root.contains_stochastic_object:
+    #
+    #         logging.warning("May be wrong for bonds bridging stochastic objects and stochastic fragments.")
+    #         _conjugated_warning = False
+    #
+    #     if self.bond_order <= 1:
+    #         return False
+    #
+    #     if self.aromatic:
+    #         return True
+    #
+    #     # a depth of two graph traversal looking for a double or triple bond
+    #     for atom in self:
+    #         for bond in atom.bonds:
+    #             if bond.id_ == self.id_:
+    #                 continue  # skip self
+    #             for atom_ in bond:
+    #                 if atom_.id_ == atom.id_:
+    #                     continue  # skip if past atom
+    #                 for bond_ in atom_.bonds:
+    #                     if bond_.bond_order > 1:
+    #                         return True
+    #
+    #     return False
+
+
+class BondDescriptor:
+    """
+    this class represents a bonding descriptor
+    """
+    __slots__ = ["parent", "descriptor", "index_", "_instances", "_instances_up_to_date", "bond_symbol", "__dict__"]
+    _eq_attr = ("descriptor", "index_", "bond_symbol")
+
+    def __init__(self,
+                 parent: StochasticObject,
+                 descriptor: str,
+                 index_: int,
+                 bond_symbol: str | None,
+                 **kwargs
+                 ):
+        """
+
+        Parameters
+        ----------
+        parent: StochasticObject
+            the owner of the bonding descriptor
+        descriptor: str
+            descriptor symbol [$, >, <]
+        index_: int
+            descriptor index [1, inf]
+        bond_symbol: str | None
+            bond symbol
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.parent = parent
+        self.descriptor = descriptor
+        self.index_ = index_
+
+        self._instances_up_to_date: bool = True
+        self._instances: list[BondDescriptorAtom] = []
+        self.bond_symbol: str | None = bond_symbol
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(BondDescriptor,), skip_color=True)
+
+    def __eq__(self, other: BondDescriptor):
+        """
+        equality is based on the following parameters:
+
+        * descriptor
+        * index_
+        * bond_symbol
+        """
+        if not isinstance(other, BondDescriptor):
+            return False
+
+        for attr in self._eq_attr:
+            if getattr(self, attr) != getattr(other, attr):
+                return False
+
+        return True
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        text = "[" + self.descriptor
+        if self.index_ == 1 and not Config.show_bond_descriptor_one_index:
+            text += ""
+        else:
+            text += str(self.index_)
+        text += "]"
+
+        return text
+
+    @property
+    def details(self) -> str:
+        """ long string representation """
+        text = str(self) + "  {"
+        for attr in self._eq_attr:
+            text += f"{attr}: {getattr(self, attr)}, "
+        return text[:-1] + "}"
+
+    @property
+    def symbol(self) -> str:
+        """ descriptor + index """
+        return self.descriptor + str(self.index_)
+
+    def is_pair(self, bd: BondDescriptor) -> bool:
+        """ Returns true if symbols are <> and index match. """
+        if self.index_ == bd.index_:
+            if (self.descriptor == "<" and bd.descriptor == ">") or \
+                    (self.descriptor == ">" and bd.descriptor == "<"):
+                return True
+
+        return False
+
+    @property
+    def root(self) -> BigSMILES:
+        """ the owner at the top of the parent tree """
+        return self.parent.root
+
+    @property
+    def instances(self) -> list[BondDescriptorAtom]:
+        """ list of BondDescriptorAtom that are based on this bonding descriptor """
+        return self._instances
+
+    @instances.setter
+    def instances(self, instances: list[BondDescriptor]):
+        self._instances = instances
+        self._instances_up_to_date = False
+
+    @property
+    def bond_order(self) -> int | None:
+        """ number of covalent bonds represented """
+        return chemical_data.bond_mapping[self.bond_symbol]
+
+    @property
+    def implicit(self) -> bool:
+        """ returns true is it is an implicit bonding descriptor """
+        if self.descriptor == "":
+            return True
+        return False
+
+    @property
+    def aromatic(self) -> bool:
+        """ limited accuracy """
+        return self.bond_symbol == ":"
+
+
+class BondDescriptorAtom:
+    """
+    this class represents a bonding descriptor and has the ability to bond
+    it should be thought of as an atom with only on bonding site that can be any bond (single, double triple)
+    """
+    __slots__ = ["descriptor", "id_", "_bond", "__dict__", "parent"]
+
+    def __init__(self,
+                 id_: int,
+                 bond_descriptor: BondDescriptor,
+                 parent: BigSMILES | Branch | StochasticFragment | None = None,
+                 **kwargs
+                 ):
+        """
+        Parameters
+        ----------
+        id_: int
+            id of bonding descriptor atom (id is limited to bonding descriptor atoms). Range: [1, inf]
+        bond_descriptor: BondDescriptor
+            bonding descriptor that this atom represents
+        parent: BigSMILES | Branch | StochasticFragment | None
+            the owner of the bonding descriptor atom
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.descriptor = bond_descriptor
+        bond_descriptor.instances += [self]
+        self.id_ = id_
+        self._bond = None  # Added after construction
+        self.parent = parent
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(BondDescriptorAtom,), skip_color=True)
+
+    def __eq__(self, other: BondDescriptorAtom):
+        """
+        equality is based on the following parameters:
+
+        * id_
+        * descriptor
+        * bond.id_
+        """
+        if not isinstance(other, BondDescriptorAtom):
+            return False
+
+        if self.id_ != other.id_:
+            return False
+
+        if self.descriptor != other.descriptor:
+            return False
+
+        if self.bond is None:
+            if other.bond is not None:
+                return False
+        else:
+            if other.bond is None or self.id_ != other.id_:
+                return False
+
+        return True
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        text = Config.add_color(self.descriptor.to_string(show_hydrogens, show_repr), 'Green', skip_color)
+
+        if show_repr is not None and BondDescriptorAtom in show_repr:
+            text += "{" + str(self.id_) + "}"
+
+        return text
+
+    @property
+    def details(self) -> str:
+        """ long string representation """
+        text = str(self) + "  {"
+        text += f"id_: {self.id_}"
+        return text + "}"
+
+    @property
+    def bond(self) -> Bond:
+        """ bond to bonding descriptor atom """
+        return self._bond
+
+    @bond.setter
+    def bond(self, bond: Bond):
+        if self.bond is not None:
+            raise errors.ConstructorError(f"Trying to make a bond to {self} when it already has a bond.")
+        self._bond = bond
+
+    @property
+    def root(self) -> BigSMILES:
+        """ the owner at the top of the parent tree """
+        return self.parent.root
+
+    @property
+    def aromatic(self) -> bool:
+        """ limited accuracy """
+        return self.descriptor.aromatic
+
+
+class Branch:
+    """
+    this class represents a branch
+    """
+    __slots__ = ["nodes", "id_", "parent", "__dict__"]
+
+    def __init__(self, id_: int, parent: BigSMILES | StochasticFragment | Branch, **kwargs):
+        """
+        Parameters
+        ----------
+        id_: int
+            id of bond (id is limited to bonds). Range: [1, inf]
+        parent: BigSMILES | StochasticFragment | Branch
+            owner of the branch
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.nodes: list[Atom | Bond | Branch | StochasticObject | BondDescriptorAtom] = []
+        self.id_ = id_
+        self.parent = parent
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(Branch,), skip_color=True)
+
+    def __eq__(self, other: Branch):
+        """ equality is determining all nodes and rings are equal """
+        if not isinstance(other, Branch):
+            return False
+
+        for node, other_node in zip(self.nodes, other.nodes):
+            if node != other_node:
+                return False
+
+        return True
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        text = "".join(node.to_string(show_hydrogens, show_atom_index, show_repr, skip_color) for node in self.nodes)
+
+        if show_repr is not None and Branch in show_repr:
+            text += "{" + str(self.id_) + "}"
+
+        return "(" + text + ")"
+
+    @property
+    def details(self) -> str:
+        """ long string representation """
+        text = str(self) + "  {"
+        text += f"id_: {self.id_}, "
+        text += f"num_nodes: {len(self.nodes)}"
+        return text + "}"
+
+    @property
+    def in_stochastic_object(self) -> bool:
+        """ returns that if on the tree up to the root there is a stochastic object parent """
+        return self.parent.in_stochastic_object
+
+    @property
+    def root(self) -> BigSMILES:
+        """ the owner at the top of the parent tree """
+        return self.parent.root
+
+    def _get_id(self, node_type) -> int:
+        return self.root._get_id(node_type)  # noqa
+
+
+class StochasticFragment:
+    """
+    this class represents a stochastic fragment.
+
+    stochastic fragments are only found within stochastic objects
+    """
+    __slots__ = ["nodes", "id_", "parent", "bonding_descriptors", "rings", "__dict__"]
+
+    def __init__(self, id_: int, parent: StochasticObject, **kwargs):
+        """
+        Parameters
+        ----------
+        id_: int
+            id of bond (id is limited to bonds). Range: [1, inf]
+        parent
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.id_ = id_
+        self.parent = parent
+        self.nodes: list[Atom | Bond | BondDescriptorAtom | Branch | StochasticObject] = []
+        self.rings: list[Bond] = []
+        self.bonding_descriptors: list[BondDescriptor] = []
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(StochasticFragment,), skip_color=True)
+
+    def __eq__(self, other: StochasticFragment):
+        """ equality is determining all nodes and rings are equal """
+        if not isinstance(other, StochasticFragment):
+            return False
+
+        for node, other_node in zip(self.nodes, other.nodes):
+            if node != other_node:
+                return False
+
+        for ring, other_ring in zip(self.rings, other.rings):
+            if ring != other_ring:
+                return False
+
+        return True
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        text = "".join(node.to_string(show_hydrogens, show_atom_index, show_repr, skip_color) for node in self.nodes)
+
+        if show_repr is not None and StochasticFragment in show_repr:
+            text += "{" + str(self.id_) + "}"
+
+        return text
+
+    @property
+    def details(self) -> str:
+        """ long string representation """
+        text = str(self) + "  {"
+        text += f"id_: {self.id_}, "
+        text += f"num_nodes: {len(self.nodes)}"
+        return text + "}"
+
+    @property
+    def in_stochastic_object(self) -> bool:
+        """ returns that if on the tree up to the root there is a stochastic object parent """
+        return self.parent.in_stochastic_object
+
+    @property
+    def root(self) -> BigSMILES:
+        """ the owner at the top of the parent tree """
+        return self.parent.root
+
+    def _get_id(self, node_type) -> int:
+        return self.root._get_id(node_type)  # noqa
+
+
+class StochasticObject:
+    """
+    this class is represents a stochastic object
+    """
+    __slots__ = ["nodes", "bonding_descriptors", "id_", "parent", "bd_left", "bd_right",
+                 "_bond_left", "_bond_right", "__dict__"]
+
+    def __init__(self, id_: int, parent: BigSMILES | StochasticFragment | Branch, **kwargs):
+        """
+        Parameters
+        ----------
+        id_: int
+            id of bond (id is limited to bonds). Range: [1, inf]
+        parent: BigSMILES | StochasticFragment | Branch
+            the owner of the atom
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.nodes: list[StochasticFragment] = []
+        self.bonding_descriptors: list[BondDescriptor] = []
+        self.bd_left: BondDescriptor | None = None
+        self.bd_right: BondDescriptor | None = None
+        self.id_ = id_
+        self._bond_left: Bond | None = None
+        self._bond_right: Bond | None = None
+        self.parent = parent
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(StochasticObject,), skip_color=True)
+
+    def __eq__(self, other: StochasticObject):
+        """ equality is determining all nodes and bonding descriptors are equal """
+        if not isinstance(other, StochasticObject):
+            return False
+
+        for node, other_node in zip(self.nodes, other.nodes):
+            if node != other_node:
+                return False
+
+        if self.bd_left != other.bd_left:
+            return False
+
+        if self.bd_right != other.bd_right:
+            return False
+
+        return True
+
+    @property
+    def details(self) -> str:
+        """ long string representation """
+        text = str(self) + "  {"
+        text += f"id_: {self.id_}, "
+        text += f"num_nodes: {len(self.nodes)}"
+        return text + "}"
+
+    @property
+    def bond_left(self) -> Bond | None:
+        """ bond leaving the left of the stochastic object """
+        return self._bond_left
+
+    @bond_left.setter
+    def bond_left(self, bond: Bond):
+        if self._bond_left is not None:
+            raise errors.ConstructorError(f"Trying to make a bond to {self} when it already has a bond.")
+        self._bond_left = bond
+
+    @property
+    def bond_right(self) -> Bond | None:
+        """ bond leaving the right of the stochastic object """
+        return self._bond_right
+
+    @bond_right.setter
+    def bond_right(self, bond: Bond):
+        if self._bond_right is not None:
+            raise errors.ConstructorError(f"Trying to make a bond to {self} when it already has a bond.")
+        self._bond_right = bond
+
+    @property
+    def aromatic(self) -> bool:
+        """ limited accuracy """
+        if self.bd_left.bond_order == 1.5:
+            # not checking right bond descriptor, maybe we should? but during construction it is not defined
+            return True
+        return False
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        text = ""
+        text += Config.add_color("{", "Red", skip_color)
+        if self.bd_left is not None:
+            text += self.bd_left.to_string(show_hydrogens, show_atom_index, show_repr, skip_color)
+        text += ",".join(node.to_string(show_hydrogens, show_atom_index, show_repr, skip_color) for node in self.nodes)
+        if self.bd_right is not None:
+            text += self.bd_right.to_string(show_hydrogens, show_atom_index, show_repr, skip_color)
+        text += Config.add_color("}", "Red", skip_color)
+        if self.bond_right is not None and self.bond_right.ring_id is not None:
+            if self.bond_right.symbol != ":":
+                text += self.bond_right.symbol
+            text += str(self.bond_right.ring_id)
+
+        if show_repr is not None and StochasticObject in show_repr:
+            text += "{" + str(self.id_) + "}"
+
+        return text
+
+    @property
+    def implicit_endgroups(self) -> bool:
+        """ Returns true if one or more are implicit """
+        if (self.bd_left is not None and self.bd_left.implicit) or \
+                (self.bd_right is not None and self.bd_right.implicit):
+            return True
+
+        return False
+
+    @property
+    def in_stochastic_object(self) -> bool:
+        """ returns True as it is the stochastic object """
+        return True
+
+    @property
+    def root(self) -> BigSMILES:
+        """ the owner at the top of the parent tree """
+        return self.parent.root
+
+    @property
+    def bonds(self) -> list[Bond]:
+        """ bonds entering the left and right """
+        bonds = []
+        if self.bond_left is not None:
+            bonds.append(self.bond_left)
+        if self.bond_right is not None:
+            bonds.append(self.bond_right)
+        return bonds
+
+    def _get_id(self, node_type) -> int:
+        return self.root._get_id(node_type)  # noqa
+
+
+def contains_stochastic_object(nodes: list[Atom, Bond, Branch, StochasticObject]):
+    """ recursive search for a stochastic_object. """
+    for node in nodes:
+        if isinstance(node, StochasticObject):
+            return True
+        if isinstance(node, Branch):
+            result = contains_stochastic_object(node.nodes)
+            if result:
+                return True
+
+    return False
+
+
+class BigSMILES:
+    """
+    this class is used to represent a BigSMILES.
+
+    BigSMILES is a superset of SMILES; so any this class handles SMILES as well
+
+    Examples
+    --------
+    bigsmiles("C1CCCCC1")
+
+    bigsmiles("CC(CC){[>][<]CC(c1ccccc1)[>][<]}[H]")
+
+    """
+    __slots__ = ["nodes", "atoms", "bonds", "rings", "__dict__"]
+
+    def __init__(self, text: str | None = None, **kwargs):
+        """
+        Parameters
+        ----------
+        text: str | None
+            BigSMILES or SMILES string. String will be processed automatically. (pass 'None' if using 'constructor')
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.nodes: list[Atom | Bond | StochasticObject | Branch] = []
+        self.atoms: list[Atom | BondDescriptorAtom] = []  # includes atoms in sub-objects
+        self.bonds: list[Bond] = []  # includes bonds in sub-objects
+        self.rings: list[Bond] = []  # does not include rings in sub-objects
+
+        self._ids_ = {Atom: 0, Bond: 0, BondDescriptorAtom: 0, StochasticFragment: 0, StochasticObject: 0, Branch: 0}
+
+        # parse input string
+        if text:
+            # import here to avoid circular imports
+            from bigsmiles.constructors.constructor_tokens import parse_bigsmiles_str
+            parse_bigsmiles_str(text, self)
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(Atom, Bond, Branch, StochasticFragment, StochasticObject, BondDescriptorAtom),
+                              skip_color=True)
+
+    def __getitem__(self, obj: int | slice) -> Atom | list[Atom]:
+        """ get items by node index or slice """
+        return self.nodes[obj]
+
+    def __iter__(self):
+        """ iterator over nodes """
+        return iter(self.nodes)
+
+    def __bool__(self):
+        """ returns True if BigSMILES has nodes """
+        if self.nodes:
+            return True
+        else:
+            return False
+
+    def __eq__(self, other: BigSMILES):
+        """ equality is determining all nodes and rings are equal """
+        if not isinstance(other, BigSMILES):
+            return False
+
+        for node, other_node in zip(self.nodes, other.nodes):
+            if node != other_node:
+                return False
+
+        for ring, other_ring in zip(self.rings, other.rings):
+            if ring != other_ring:
+                return False
+
+        return True
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        return "".join(node.to_string(show_hydrogens, show_atom_index, show_repr, skip_color) for node in self.nodes)
+
+    @property
+    def details(self) -> str:
+        """ long string representation """
+        text = str(self) + "  {"
+        text += f"num_nodes: {len(self.nodes)}, "
+        text += f"num_atoms: {len(self.atoms)}, "
+        text += f"num_bonds: {len(self.bonds)}"
+        return text + "}"
+
+    @property
+    def in_stochastic_object(self) -> bool:
+        """ returns False as BigSMILES is always a root node and can't be contained in anything """
+        return False
+
+    @property
+    def contains_stochastic_object(self) -> bool:
+        """ returns True if a stochastic object is present in the BigSMILES (anywhere) """
+        return contains_stochastic_object(self.nodes)
+
+    @property
+    def has_disconnect(self) -> bool:
+        """ returns True if '.' (disconnect symbol) is present in the BigSMILES (anywhere) """
+        for bond in self.bonds:
+            if bond.symbol == ".":
+                return True
+        return False
+
+    @property
+    def root(self) -> BigSMILES:
+        """ root will return itself as BigSMILES object is always the root node """
+        return self
+
+    def _get_id(self, node_type) -> int:
+        self._ids_[node_type] += 1
+        return self._ids_[node_type]
+
+
+# types  (for python >3.10)
+# has_node_attr = BigSMILES | Branch | StochasticObject | StochasticFragment
+# has_ring_attr = BigSMILES | StochasticFragment
+# has_parent_attr = Branch | StochasticObject | StochasticFragment | Bond | BondDescriptorAtom | Atom
+# has_root_attr = BigSMILES | has_parent_attr
+
+# types (for python <=3.9)
+import typing
+
+bigsmiles_obj = [Atom, Bond, Branch, BondDescriptorAtom, StochasticObject, StochasticFragment, BigSMILES]
+has_node_attr = typing.Union[BigSMILES, Branch, StochasticObject, StochasticFragment]
+has_ring_attr = typing.Union[BigSMILES, StochasticObject]
+has_parent_attr = typing.Union[Branch, StochasticObject, StochasticFragment, Bond, BondDescriptorAtom, Atom]
+has_root_attr = typing.Union[BigSMILES, has_parent_attr]
diff --git a/src/bigsmiles/data_structures/molecular_formula.py b/src/bigsmiles/data_structures/molecular_formula.py
new file mode 100644
index 0000000..2cc4fdd
--- /dev/null
+++ b/src/bigsmiles/data_structures/molecular_formula.py
@@ -0,0 +1,144 @@
+from __future__ import annotations
+import re
+
+class MolecularFormula:
+    def __init__(self, formula: str):
+        self._formula = None
+        self._elements = None
+
+        self.formula = formula
+
+    def __str__(self):
+        return str(self.formula)
+
+    def __repr__(self):
+        return str(self.formula)
+
+    def __getitem__(self, key: str):
+        if key in self._elements:
+            return self._elements[key]
+
+    def __iter__(self):
+        for element, num in self._elements.items():
+            yield element, num
+
+    @property
+    def formula(self):
+        return self._formula
+
+    @formula.setter
+    def formula(self, formula: str):
+        if not isinstance(formula, str):
+            raise TypeError(f"'formula' must be a str. ({type(formula)} given)")
+
+        self._formula, self._elements = self.reduce(formula)
+
+    @property
+    def elements(self):
+        return self._elements
+
+    def reduce(self, formula: str) -> (str, dict[str, int]):
+        """
+        Reduces all chemical formulas to the same reduced version.
+        :param formula:
+        :return:
+        """
+        split_formula = self._split_formula(formula)
+        elements = self._split_formula_to_element_dict(split_formula)
+        formula = self._element_to_formula(elements, config_chemical.molecular_formula_element_order)
+
+        return formula, elements
+
+    @staticmethod
+    def _split_formula(formula: str) -> list[str]:
+        """
+        Split formula up into letters and numbers.
+        Example: "C1H3OH63Cr2CCCOOO" -> ['C', '1', 'H', '3', 'O', 'H', '63', 'Cr', '2', 'C', 'C', 'C', 'O', 'O', 'O']
+        """
+        pattern = r'[A-Z]{1}[a-z]*|\d+'
+        return re.findall(pattern, formula)
+
+    @staticmethod
+    def _split_formula_to_element_dict(split_formula: list[str]) -> dict[str, int]:
+        """
+        Given a split chemical formula return a dictionary of element_symbols.
+        :param split_formula:
+        :return: {"symbol symbol": "count"} -> {"C": 1, "O": 2}
+        """
+        # put data into dictionary
+        elements = {}
+        for index, entry in enumerate(split_formula):
+            if not entry.isnumeric():
+                if entry not in periodic_table.symbols:
+                    raise MolecularFormulaError(f"Invalid symbol in chemical formula. Invalid: {entry}")
+
+                if entry in elements:
+                    if index + 1 < len(split_formula):
+                        num_ = split_formula[index + 1]
+                    else:
+                        num_ = "1"
+                    if num_.isnumeric():
+                        elements[entry] = int(num_) + elements[entry]
+                    else:
+                        elements[entry] = 1 + elements[entry]
+                else:
+                    if index + 1 < len(split_formula):
+                        num_ = split_formula[index + 1]
+                    else:
+                        num_ = "1"
+                    if num_.isnumeric():
+                        elements[entry] = int(num_)
+                    else:
+                        elements[entry] = 1
+
+        return elements
+
+    def _element_to_formula(self, elements: dict, order: list[str] = None) -> str:
+        """
+        Converts symbol dictionary into a reduced string.
+        """
+        # Set order
+        if order is None:
+            # default is hill system
+            if "C" in elements.keys():
+                order = self._set_element_order_in_formula(["C", "H"])
+            else:
+                order = self._set_element_order_in_formula()
+        else:
+            order = self._set_element_order_in_formula(order)
+
+        # Generate chemical formula
+        chemical_formula = ""
+        for element in order:
+            if element in elements.keys() and (elements[element] is not None or elements[element] == 0):
+                if elements[element] == 1:
+                    chemical_formula += element
+                else:
+                    chemical_formula += element + str(elements[element])
+
+        return chemical_formula
+
+    @staticmethod
+    def _set_element_order_in_formula(order: list[str] = None) -> list[str]:
+        """Given a chemical order of element_symbols, validate it/ and complete it. Default is alphabetically."""
+        base = periodic_table.symbols_alphabetical_order()
+        if order is None:
+            return base
+
+        for k in reversed(order):
+            if k in base:
+                base.remove(k)
+                base.insert(0, k)
+            else:
+                raise MolecularFormulaError(f"Invalid symbol in 'order' for chemical formula. Invalid: {k}")
+
+        return base
+
+
+def local_run():
+    cf = MolecularFormula("C1H3OH63Cr2CCCOOO")
+    print(cf)
+
+
+if __name__ == '__main__':
+    local_run()
diff --git a/src/bigsmiles/data_structures/reaction.py b/src/bigsmiles/data_structures/reaction.py
new file mode 100644
index 0000000..7eed32a
--- /dev/null
+++ b/src/bigsmiles/data_structures/reaction.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+from bigsmiles.data_structures.bigsmiles import BigSMILES, Atom, Bond, Branch, StochasticFragment, \
+    StochasticObject, BondDescriptorAtom
+
+
+class Reaction:
+    """
+
+    data structure for reaction BigSMILES
+
+    !!! info "Accepted patterns"
+
+        * reactant '>' agent '>' product
+        * reactant '>>' product
+        * reactant.reactant>>product.product
+        * reactant,reactant>>product,product (comma supported for easier parsing)
+
+    !!! info "Definitions"
+        **^^reactants:^^** materials that contributing one or more atoms to the product
+
+        **^^agents:^^** materials that don't contribute any atoms to the product or receive atoms from the reactant
+        (catalysts, solvents)
+
+        **^^product:^^** output of reactions. all atoms should have come from the reactants
+
+    """
+    __slots__ = ["reactants", "agents", "products", "__dict__"]
+
+    def __init__(self, text: str | None = None, **kwargs):
+        """
+
+        Parameters
+        ----------
+        text: str
+            Reaction BigSMILES string
+        kwargs:
+            any additional keyword arguments are accepted and set as additional attributes
+        """
+        self.reactants: list[BigSMILES] = []
+        self.agents: list[BigSMILES] = []
+        self.products: list[BigSMILES] = []
+
+        # parse input string
+        if text:
+            from bigsmiles.constructors.constructor_reaction import parse_reaction
+            self.reactants, self.agents, self.products = parse_reaction(text)
+
+        if kwargs:
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+    def __str__(self):
+        return self.to_string()
+
+    def __repr__(self):
+        return self.to_string(show_repr=(Atom, Bond, Branch, StochasticFragment, StochasticObject, BondDescriptorAtom),
+                              skip_color=True)
+
+    def to_string(self,
+                  show_hydrogens: bool = False,
+                  show_atom_index: bool = True,
+                  show_repr: tuple | None = None,
+                  skip_color: bool = False
+                  ) -> str:
+        text = ""
+        text += ",".join(chem.to_string(show_hydrogens, show_atom_index, show_repr, skip_color)
+                         for chem in self.reactants)
+        if self.agents:
+            text += ">"
+            text += ",".join(chem.to_string(show_hydrogens, show_atom_index, show_repr, skip_color)
+                             for chem in self.agents)
+            text += ">"
+        else:
+            text += ">>"
+        text += ",".join(chem.to_string(show_hydrogens, show_atom_index, show_repr, skip_color)
+                         for chem in self.products)
+
+        return text
diff --git a/src/bigsmiles/data_structures/stereo_rules.py b/src/bigsmiles/data_structures/stereo_rules.py
new file mode 100644
index 0000000..0212d06
--- /dev/null
+++ b/src/bigsmiles/data_structures/stereo_rules.py
@@ -0,0 +1,497 @@
+"""
+
+Code for determining double bond E/Z stereo-chemistry
+
+"""
+
+from __future__ import annotations
+
+import enum
+import logging
+
+import bigsmiles.reference_data.chemical_data as chemical_data
+from bigsmiles.data_structures.bigsmiles import Bond, Atom, StochasticObject, BondDescriptorAtom
+
+
+MAX_DEPTH = 100  # 100 is the max depth of traversal
+DEFAULT_SCORE = 150
+
+
+def get_other_node(bond: Bond, not_this_node: Atom | StochasticObject) \
+        -> Atom | StochasticObject | BondDescriptorAtom | None:
+    for atom in bond:
+        if atom is not not_this_node:
+            return atom
+
+    return None
+
+
+def get_other_bonds(atom: Atom, not_this_bond: tuple[Bond, ...] | list[Bond, ...]) -> list[Bond, ...]:
+    bonds = []
+    for bond in atom.bonds:
+        if bond not in not_this_bond:
+            bonds.append(bond)
+
+    return bonds
+
+
+def get_bond_with_ez_symbol(atom: Atom) -> Bond | None:
+    ez_bond = None
+    for bond in atom.bonds:
+        if bond.symbol == "/" or bond.symbol == "\\":
+            if ez_bond is not None:
+                logging.warning(r"Extra '\' or '\\' detected." +
+                                f"\nIgnoring symbol: {ez_bond.details} \nBigSMILES: {atom.root}")
+
+            ez_bond = bond
+
+    return ez_bond
+
+
+class PriorityScore:
+    __slots__ = ["atomic_number", "next_atom_highest_atomic_number", "number_bonds"]
+
+    def __init__(self, atomic_number: int, next_atom_highest_atomic_number: int, number_bonds: float | int):
+        self.atomic_number = atomic_number
+        self.next_atom_highest_atomic_number = next_atom_highest_atomic_number
+        self.number_bonds = number_bonds
+
+    def __str__(self):
+        return ",".join((str(getattr(self, name)) for name in self.__slots__))
+
+    def __eq__(self, other):
+        if self.atomic_number != other.atomic_number or self.number_bonds != other.number_bonds:
+            return False
+
+        return True
+
+    def __lt__(self, other):
+        if self.atomic_number < other.atomic_number:
+            return True
+
+        if self.atomic_number == other.atomic_number:
+            if self.next_atom_highest_atomic_number < other.next_atom_highest_atomic_number:
+                return True
+
+            if self.next_atom_highest_atomic_number == other.next_atom_highest_atomic_number:
+                if self.number_bonds < other.number_bonds:
+                    return True
+
+        return False
+
+
+bond_scores = {
+    ".": 0,
+    "": 1,
+    "-": 1,
+    "/": 1,
+    "\\": 1,
+    ":": 1.5,
+    "=": 2.1,  # little extra is to give them priority over two single bonds
+    "#": 3.2,  # little extra is to give them priority over two single bonds
+    "$": 4.1
+}
+
+
+def get_atom_score(node: Atom | StochasticObject | BondDescriptorAtom) -> int:
+    if isinstance(node, Atom) and node.symbol in chemical_data.symbol_to_atomic_number:
+        return chemical_data.symbol_to_atomic_number[node.symbol]
+
+    return DEFAULT_SCORE
+
+
+def get_score(node: Atom | StochasticObject | BondDescriptorAtom | None) -> PriorityScore:
+    if node is None:
+        return PriorityScore(0, 0, 0)
+
+    # atom score
+    atomic_number = get_atom_score(node)
+
+    # next atom score
+    next_atoms = (get_other_node(bond, node) for bond in node.bonds)
+    next_atom_highest_atomic_number = max(get_atom_score(atom) for atom in next_atoms)
+
+    # bond score
+    if isinstance(node, Atom):
+        number_bonds = sum(bond_scores[bond.symbol] for bond in node.bonds)
+    elif isinstance(node, StochasticObject):
+        number_bonds = node.bond_left.bond_order
+    elif isinstance(node, BondDescriptorAtom):
+        number_bonds = 0
+    else:
+        raise NotImplementedError("code bug")
+
+    return PriorityScore(atomic_number, next_atom_highest_atomic_number, number_bonds)
+
+
+class TraversalPath:
+
+    def __init__(self, nodes: list = None, edges: list = None):
+        self.nodes = nodes if nodes is not None else []
+        self.edges = edges if edges is not None else []
+
+    def __str__(self):
+        return "->".join(repr(node) for node in self.nodes)
+
+    def get_score(self) -> PriorityScore:
+        return get_score(self.nodes[-1])
+
+
+def next_step(path:  TraversalPath) -> list:
+    additional_paths = []
+    current_node = path.nodes[-1]
+
+    if current_node is None:
+        path.nodes.append(None)
+        return additional_paths
+
+    elif isinstance(current_node, Atom):
+        atoms = []
+        bonds = []
+        for bond in current_node.bonds:
+            if bond in path.edges:
+                continue
+            atoms.append(get_other_node(bond, current_node))
+            bonds.append(bond)
+
+        if len(atoms) == 0:
+            path.nodes.append(None)
+            return additional_paths
+
+        atoms, bonds = sort_atoms_by_score(atoms, bonds)
+        for i in range(len(atoms)):
+            if i == 0:
+                path.nodes.append(atoms[0])
+                path.edges.append(bonds[0])
+            else:
+                additional_paths.append(
+                    TraversalPath(path.nodes[:-1] + [atoms[i]], path.edges[:-1] + [bonds[i]])
+                )
+
+    elif isinstance(current_node, BondDescriptorAtom):
+        path.nodes.append(None)
+
+    elif isinstance(current_node, StochasticObject):
+        if current_node.bond_left in path.edges:
+            if current_node.bond_right is None:
+                path.nodes.append(None)
+
+            path.nodes.append(get_other_node(current_node.bond_right, current_node))
+            path.edges.append(current_node.bond_right)
+
+        else:
+            if current_node.bond_left is None:
+                path.nodes.append(None)
+
+            path.nodes.append(get_other_node(current_node.bond_left, current_node))
+            path.edges.append(current_node.bond_left)
+
+    else:
+        raise NotImplementedError("coding bug")
+
+    return additional_paths
+
+
+def sort_atoms_by_score(atoms: list[Atom, ...], bonds: list[Bond, ...]) -> tuple[list[Atom, ...], list[Bond, ...]]:
+    score = []
+    for atom in atoms:
+        score.append(get_atom_score(atom))
+
+    return [x for _, x in sorted(zip(score, atoms))], [x for _, x in sorted(zip(score, bonds))],
+
+
+class TraversalDirection:
+
+    def __init__(self, start_atom: Atom, bond_prior: Bond):
+        self.start_atom = start_atom
+        self.bond_prior = bond_prior
+        self.paths: [TraversalPath] = [TraversalPath([start_atom], [bond_prior])]
+        self.best_path = self.paths[0]
+        self._scores = None
+        self._up_to_date = False
+
+    @property
+    def num_paths(self) -> int:
+        return len(self.paths)
+
+    def scores(self) -> list[PriorityScore]:
+        if not self._up_to_date:
+            self._scores = [path.get_score() for path in self.paths]
+            index_not_none = next(i for i, score in enumerate(self._scores) if score is not None)
+            self.best_path = self.paths[index_not_none]
+            self._up_to_date = True
+
+        return self._scores
+
+    def one_traverse_step(self):
+        self._up_to_date = False
+
+        # traverse the graph one step
+        for i in range(len(self.paths)):  # index used as size of self.up may change during loop
+            additional_paths = next_step(self.paths[i])
+            self.paths += additional_paths
+
+
+class TraversalOutcomes(enum.Enum):
+    up = 1
+    down = 2
+    tie = 3
+
+
+class TraversalSide:
+    def __init__(self, double_bond: Bond, side: bool):
+        """
+        Parameters
+        ----------
+        double_bond:
+
+        side:
+            True: left, False: right
+        """
+        self.double_bond = double_bond
+        self.side = side
+
+        self.up: TraversalDirection | None = None
+        self.down: TraversalDirection | None = None
+        self.winner: TraversalOutcomes | None = None
+        self.number_of_double_bonds_in_series: int = 0  # in a row after the starting double bond
+        self.setup_successful: bool = True
+
+        self.setup()
+
+    def setup(self):
+        if self.side:
+            atom = self.double_bond.atom1
+        else:
+            atom = self.double_bond.atom2
+
+        # get starting atom
+        starting_atom = self.check_for_multiple_double_bonds(atom, self.double_bond)
+        if starting_atom is None:
+            return
+
+        # determine up down directions
+        if self.side:
+            self.determine_up_down_atom_left(starting_atom)
+        else:
+            self.determine_up_down_atom_right(starting_atom)
+
+    def check_for_multiple_double_bonds(self, atom: Atom, bond: Bond) -> Atom | None:
+        """
+        check for multiple double bonds in a row
+        Maybe recursive
+
+        Parameters
+        ----------
+        atom:
+            current atom position
+        bond:
+            prior bond
+
+        Returns
+        -------
+        atom:
+            Atom to start up/down traversals from
+
+        """
+        other_bonds = get_other_bonds(atom, [bond])
+        if not other_bonds:
+            self.setup_successful = False
+            return None  # double bond is terminated with implicit hydrogens or lone pairs
+        if len(other_bonds) == 1 and other_bonds[0].symbol == "=":
+            next_atom = get_other_node(other_bonds[0], atom)
+            self.number_of_double_bonds_in_series += 1
+            return self.check_for_multiple_double_bonds(next_atom, other_bonds[0])
+
+        return atom
+
+    def determine_up_down_atom_left(self, start_atom: Atom):
+        bond_ez = get_bond_with_ez_symbol(start_atom)
+        if bond_ez is None:
+            self.setup_successful = False
+            return  # no stereo bonds '/' or '\\'
+
+        atom_ez = get_other_node(bond_ez, start_atom)
+
+        # set up path
+        direction = TraversalDirection(atom_ez, bond_ez)
+        if bond_ez.symbol == "/":
+            if atom_ez.parent is start_atom.parent:
+                self.up = direction  # main chain
+            else:
+                self.down = direction  # branch
+        else:  # "\\"
+            if atom_ez.parent is start_atom.parent:
+                self.down = direction  # main chain
+            else:
+                self.up = direction  # branch
+
+        self.figure_out_other_bond(start_atom, bond_ez)
+
+    def determine_up_down_atom_right(self, start_atom: Atom):
+        bond_ez = get_bond_with_ez_symbol(start_atom)
+        if bond_ez is None:
+            self.setup_successful = False
+            return  # no stereo bonds '/' or '\\'
+
+        atom_ez = get_other_node(bond_ez, start_atom)
+
+        # set first atom that has stereo symbol
+        if bond_ez.symbol == "/":
+            self.down = TraversalDirection(atom_ez, bond_ez)
+        else:  # "\\"
+            self.up = TraversalDirection(atom_ez, bond_ez)
+
+        self.figure_out_other_bond(start_atom, bond_ez)
+
+    def figure_out_other_bond(self, start_atom: Atom, bond_ez: Bond):
+        # figure out other bond
+        other_bond = get_other_bonds(start_atom, (self.double_bond, bond_ez))
+        if len(other_bond) == 1:
+            other_atom = get_other_node(other_bond[0], start_atom)
+            if self.down is None:
+                self.down = TraversalDirection(other_atom, other_bond[0])
+            else:
+                self.up = TraversalDirection(other_atom, other_bond[0])
+
+        elif len(other_bond) == 0:
+            # other bond must be implicit hydrogens or terminal
+            if self.down is None:
+                self.winner = TraversalOutcomes.up
+            else:
+                self.winner = TraversalOutcomes.down
+
+        else:
+            raise NotImplementedError("more than 2 bonds coming off double bond atom")
+
+    def check_for_winner(self):
+        if self.winner:
+            return
+
+        scores_up = self.up.scores()
+        scores_down = self.down.scores()
+        for i in range(max([len(scores_up), len(scores_down)])):
+            if i > len(scores_up) - 1 and i > len(scores_up) - 1:
+                self.winner = TraversalOutcomes.tie
+            if i > len(scores_up) - 1:
+                self.winner = TraversalOutcomes.down
+                break
+            if i > len(scores_down) - 1:
+                self.winner = TraversalOutcomes.up
+                break
+
+            if scores_up[i] == scores_down[i]:
+                if scores_up is None:
+                    continue
+                else:
+                    break
+
+            if scores_up > scores_down:
+                self.winner = TraversalOutcomes.up
+            else:
+                self.winner = TraversalOutcomes.down
+
+    def run_graph_traversal(self):
+        self.check_for_winner()
+        if self.winner:
+            return
+
+        logging.debug(f" Starting '{'left' if self.side else 'right'}' E/Z graph traversal:"
+                      f" {self.double_bond.root.to_string(show_repr=(Atom,))}"
+                      f"\n-------------------------------")
+        logging.debug(f"\t {'step':>4}, {'up':<30}, {'num_up_paths':>12}, {'down':>30}, {'num_down_paths':>12}")
+        logging.debug(f"\t {0:4}, {str(self.up.best_path):<30}, {self.up.num_paths:>12}"
+                      f" {str(self.down.best_path):<30}, {self.down.num_paths:>12}")
+
+        # perform graph traversal
+        for i in range(1, MAX_DEPTH):
+            self.up.one_traverse_step()
+            self.down.one_traverse_step()
+
+            self.check_for_winner()
+            logging.debug(f"\t {i:4}, {str(self.up.best_path):<30}, {self.up.num_paths:>12}"
+                          f" {str(self.down.best_path):<30}, {self.down.num_paths:>12}")
+
+            if self.winner:
+                return
+
+        else:
+            logging.warning(f'Max traversal depth reached on E/Z determination of: '
+                            f'\nBigSMILES: {self.double_bond.parent} '
+                            f'\nBond: {repr(self.double_bond)}')
+            return
+
+
+def check_setup(left_side: TraversalSide, right_side: TraversalSide) -> bool:
+    """ Check to see if there were reasons to terminate early from traversal setup """
+    if not left_side.setup_successful or not right_side.setup_successful:
+        return True
+
+    # check for 'odd' number of double bonds in a row
+    if (left_side.number_of_double_bonds_in_series + right_side.number_of_double_bonds_in_series + 1) % 2 == 0:
+        # left + right + 1 (starting double bond) = even  --> return None for E/Z
+        logging.warning("Double bond stereo-chemistry notation '/' and '\\' should "
+                        f"not be used on even number of double bonds; they are axial chiral and use @/@@ notation on "
+                        f"center atom.\nBigSMILES:{left_side.double_bond.parent}")
+        return True
+
+    return False
+
+
+def determine_winner(left_side: TraversalSide, right_side: TraversalSide) -> str | None:
+    if left_side.winner is None or right_side.winner is None:
+        return None
+
+    if left_side.winner is TraversalOutcomes.tie and right_side.winner is TraversalOutcomes.tie:
+        logging.warning("Double bond stereo-chemistry notation '/' and '\\' should "
+                        "not be used on symmetric molecules.")
+        return None
+
+    if (left_side.winner is TraversalOutcomes.tie or right_side.winner is TraversalOutcomes.tie) or \
+            (left_side.winner == right_side.winner):
+        return "Z"
+
+    return "E"
+
+
+def get_double_bond_ez(double_bond: Bond) -> str | None:
+    """
+    Determines the E/Z stereochemistry of a double Bond
+
+    Parameters
+    ----------
+    double_bond:
+        double bond to start analysis at
+
+    Returns
+    -------
+    result:
+        will return 'E'/'Z' for odd number of double bonds, None for even number of double bonds (axial chiral)
+
+    Notes
+    -----
+
+    * Breath-first-search (BFS) prioritizing heavy atoms and following more bonds.
+    Starts out at the double bond with two traverses heading left and two right,
+    with one going up and the other going down for a total of 4 simultaneous BFS.
+    At each step the below rules are applied to compute a score
+    The searches terminate once a difference between the up-down score.
+    * Rules:
+        * rule 1: higher atomic mass has higher priority
+        * rule 2: (if same atomic mass) atom with more of these substituent
+        * rule 3: propagate down chain till there is a difference
+        * double/triple bonds are considered to be bonded to an equivalent single bonds C=C --> C(C)-C(C)
+    * stochastic objects are scored as 150 and treated as a single atom
+    * max traversal depth is 200
+
+    """
+    left_side = TraversalSide(double_bond, True)
+    right_side = TraversalSide(double_bond, False)
+
+    if check_setup(left_side, right_side):
+        return None
+
+    left_side.run_graph_traversal()
+    right_side.run_graph_traversal()
+
+    return determine_winner(left_side, right_side)
diff --git a/src/bigsmiles/errors.py b/src/bigsmiles/errors.py
index ab27339..6e2eaae 100644
--- a/src/bigsmiles/errors.py
+++ b/src/bigsmiles/errors.py
@@ -1,8 +1,29 @@
+from __future__ import annotations
 
 
 class BigSMILESError(Exception):
-    def __init__(self, text: str, optional: str = None):
-        if optional is not None:
-            text += "(" + optional + ")"
-        super().__init__(text)
+    """ base BigSMILES error """
 
+    def __init__(self, text: str, error=None):
+        if isinstance(error, BigSMILESError):
+            text += '\n\t'
+            text += error.text.replace("\t", "\t\t")
+        self.text = text
+
+    def __str__(self):
+        return self.text
+
+
+class TokenizeError(BigSMILESError):
+    """ raised when an error occurs tokenizing a BigSMILES string. """
+
+
+class ConstructorError(BigSMILESError):
+    """ raised when an error occurs during the construction of a BigSMILES string. """
+
+
+class ValidationError(BigSMILESError):
+    """ raised when validating a SMILES/BigSMILES string is syntactically correct. """
+
+
+ERRORS = (BigSMILESError, TokenizeError)
diff --git a/src/bigsmiles/methods/__init__.py b/src/bigsmiles/methods/__init__.py
new file mode 100644
index 0000000..3cfc240
--- /dev/null
+++ b/src/bigsmiles/methods/__init__.py
@@ -0,0 +1,1 @@
+from bigsmiles.methods.display_methods.tree_to_string import tree_to_string, TreeConfig
diff --git a/src/bigsmiles/methods/display_methods/__init__.py b/src/bigsmiles/methods/display_methods/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bigsmiles/tree_to_string.py b/src/bigsmiles/methods/display_methods/tree_to_string.py
similarity index 75%
rename from src/bigsmiles/tree_to_string.py
rename to src/bigsmiles/methods/display_methods/tree_to_string.py
index 7ccf5d3..51d7fe8 100644
--- a/src/bigsmiles/tree_to_string.py
+++ b/src/bigsmiles/methods/display_methods/tree_to_string.py
@@ -3,11 +3,28 @@
 Code for turing a python class into a terminal printable tree.
 
 """
+from __future__ import annotations
 
-from typing import Protocol
+from bigsmiles.data_structures.bigsmiles import Atom, Bond, BondDescriptorAtom, Branch, StochasticFragment, \
+    StochasticObject, BigSMILES
 
 
 class TreeConfig:
+    """
+    this class can be used to configure the tree parsing output
+
+    Parameters
+    ----------
+    tree_symbol_options: dict
+        symbols to build the tree. various ascii options are available
+    symbols: str
+        select from 'tree_symbol_options' what ascii symbols you want to use
+
+    Notes
+    -----
+    * color can be added to the output with through the main configuration class
+
+    """
     tree_symbol_options = {
         'ascii': ('|', '|-- ', '+-- '),
         'ascii-ex': ('\u2502', '\u251c\u2500\u2500', '\u2514\u2500\u2500'),
@@ -19,6 +36,15 @@ class TreeConfig:
     symbols = 'ascii-ex'  # some computers may need to show different symbols
     tab = "    "
     space = " "
+    printed_classes = {
+        Atom: True,
+        Bond: True,
+        BondDescriptorAtom: True,
+        Branch: False,
+        StochasticFragment: False,
+        StochasticObject: False,
+        BigSMILES: False
+    }
 
     @classmethod
     def vertical_line(cls) -> str:
@@ -33,14 +59,6 @@ def corner(cls) -> str:
         return cls.tree_symbol_options[cls.symbols][2]
 
 
-class TreeNode(Protocol):
-    _tree_print_repr: bool
-
-
-class TreeIntermediateNode(TreeNode):
-    nodes: list
-
-
 def to_repr(obj) -> str:
     return repr(obj)
 
@@ -49,13 +67,13 @@ def to_str(obj) -> str:
     return str(obj)
 
 
-def tree_to_string(root_node: TreeIntermediateNode, show_object_labels: bool = True, print_repr: bool = False) -> str:
+def tree_to_string(bigsmiles_: BigSMILES, show_object_labels: bool = True, print_repr: bool = False) -> str:
     """
     Creates a string representation of a tree for printing.
 
     Parameters
     ----------
-    root_node: TreeIntermediateNode
+    bigsmiles_: BigSMILES
         Root node
     show_object_labels: bool
         show object labels
@@ -74,18 +92,18 @@ def tree_to_string(root_node: TreeIntermediateNode, show_object_labels: bool = T
 
     # add root node
     if show_object_labels:
-        text = type(root_node).__name__ + ": "
+        text = type(bigsmiles_).__name__ + ": "
 
-    text += str(root_node)
+    text += str(bigsmiles_)
 
     # add main part of tree
-    text += tree_to_string_loop(root_node.nodes, [], show_object_labels, to_repr if print_repr else to_str)
+    text += tree_to_string_loop(bigsmiles_.nodes, [], show_object_labels, to_repr if print_repr else to_str)
 
     return text
 
 
 def tree_to_string_loop(
-        nodes: list[TreeNode, TreeIntermediateNode],
+        nodes: list,
         spacers: list[bool],
         show_object_label: bool,
         func: callable
@@ -95,7 +113,7 @@ def tree_to_string_loop(
 
     Parameters
     ----------
-    nodes: list[TreeNode | TreeIntermediateNode]
+    nodes: list
         nodes
     spacers: list[bool]
         list records weather a continuation line (TreeConfig.vertical_line) is needed in the tree
@@ -129,7 +147,7 @@ def tree_to_string_loop(
 
 
 def create_row(
-        node: TreeNode | TreeIntermediateNode,
+        node,
         spacers: list[bool],
         show_object_label: bool,
         func: callable,
@@ -177,7 +195,7 @@ def create_row(
         line += type(node).__name__ + ": "
 
     # add node symbol
-    if node._tree_print_repr:
+    if TreeConfig.printed_classes[type(node)]:
         line += func(node)
     else:
         line += str(node)
diff --git a/src/bigsmiles/methods/graph/__init__.py b/src/bigsmiles/methods/graph/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bigsmiles/methods/graph/adjacency_list.py b/src/bigsmiles/methods/graph/adjacency_list.py
new file mode 100644
index 0000000..833e774
--- /dev/null
+++ b/src/bigsmiles/methods/graph/adjacency_list.py
@@ -0,0 +1,18 @@
+
+
+
+def adjacency_list(bigsmiles_: BigSMILES):
+    """
+    a collection of unordered lists used to represent a finite graph
+    Parameters
+    ----------
+    bigsmiles_
+
+    Returns
+    -------
+
+    """
+    adjacency_list = []
+
+
+
diff --git a/src/bigsmiles/methods/graph/adjacency_matrix.py b/src/bigsmiles/methods/graph/adjacency_matrix.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bigsmiles/parse_bigsmiles_str.py b/src/bigsmiles/parse_bigsmiles_str.py
deleted file mode 100644
index 2db7b5f..0000000
--- a/src/bigsmiles/parse_bigsmiles_str.py
+++ /dev/null
@@ -1,176 +0,0 @@
-import warnings
-
-
-def custom_formatwarning(msg, *args, **kwargs):
-    # ignore everything except the message
-    return str(msg) + '\n'
-
-warnings.formatwarning = custom_formatwarning
-
-from bigsmiles.validation import pre_validation
-from bigsmiles.tokenizer import Token, TokenKind, tokenize
-from bigsmiles.constructor import States
-from bigsmiles.constructor_str import BigSMILESStringConstructor
-from bigsmiles.errors import BigSMILESError
-
-
-def map_atom(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token):
-    if constructor.state is States.start:
-        constructor.add_atom(token.value)
-
-    elif constructor.state in (States.atom, States.bond_descriptor, States.stochastic_object_end,
-                               States.branch_start, States.ring):
-        constructor.add_bond_atom_pair("", token.value)  # add single bond
-    elif constructor.state in (States.stochastic_fragment,):
-        constructor.add_atom(token.value)
-
-    else:
-        raise BigSMILESError("Something went wrong in 'map_atom'.")  # should not be hit
-
-
-def map_bond(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token):
-    try:
-        next_token = tokens.pop(0)
-    except IndexError:
-        raise BigSMILESError("Bond can't be at the end of a BigSMILES string.")
-
-    if next_token.kind in (TokenKind.Atom, TokenKind.AtomExtend):
-        constructor.add_bond_atom_pair(token.value, next_token.value)
-    elif next_token.kind is not TokenKind.BondDescriptor:
-        constructor.add_bond_bonding_descriptor_pair_str(token.value, next_token.value)
-    elif next_token.kind is not TokenKind.StochasticStart:
-        map_stochastic_object_start(constructor, tokens, next_token, token)
-    else:
-        raise BigSMILESError(f"Bonds must be followed by an Atom, Bonding Descriptor, or Stocastic Object. "
-                             f"Parse completed: {constructor.bigsmiles}, issue token: {token}")
-
-
-def map_bond_descriptor(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token):
-    if tokens[0].kind is TokenKind.StochasticEnd:
-        constructor.close_stochastic_fragment()
-        constructor.close_stochastic_object_str(token.value)
-        tokens.pop(0)
-        return
-
-    if constructor.state is States.stochastic_fragment:
-        # first StochasticFragment symbol
-        constructor.add_bonding_descriptor_str(token.value)
-        return
-
-    if constructor.state is States.branch_start:
-        # first Branch symbol
-        # check to make sure the branch closes immediately
-        if tokens[0].kind != TokenKind.BranchEnd:
-            raise BigSMILESError("If Bonding Descriptors is the first Branch symbol, it can only be followed by "
-                                 "Branch End.")
-    constructor.add_bond_bonding_descriptor_pair_str("", token.value)
-
-
-def map_branch_start(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token):
-    constructor.open_branch()
-
-
-def map_branch_end(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token):
-    constructor.close_branch()
-
-
-def map_ring(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token):
-    if constructor.state is not States.atom:
-        raise BigSMILESError(f"Ring number must follow atoms.")
-
-    constructor.add_ring(int(token.value.replace('%', '')))
-
-
-def map_stochastic_object_start(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token,
-                                bond_token: Token = None):
-    try:
-        next_token = tokens.pop(0)
-    except IndexError:
-        raise BigSMILESError("Stochastic objects must begin with a bond descriptor (or implicit bonding descriptor).")
-
-    if next_token.kind not in (TokenKind.ImplictEndGroup, TokenKind.BondDescriptor):
-        raise BigSMILESError(f"Stochastic object starts must be followed an explict or implicit end group.")
-
-    if constructor.state is States.start:
-        constructor.open_stochastic_object_str(next_token.value)
-    else:
-        if bond_token is None:
-            constructor.open_stochastic_object_with_bond_str("", next_token.value)
-        else:
-            constructor.open_stochastic_object_with_bond_str(bond_token.value, next_token.value)
-
-
-def map_stochastic_object_end(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token) -> TokenKind:
-    raise BigSMILESError("Stochastic objects should end with bonding descriptor (or implicit bonding description)")
-
-
-def map_bond_seperator(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token):
-    constructor.close_open_stochastic_fragment()
-
-
-def NotImplementedFunc(*args, **kwargs):
-    raise NotImplementedError
-
-
-def SkipSymbol(constructor: BigSMILESStringConstructor, tokens: list[Token], token: Token):
-    warnings.warn(f"Symbol skipped: {token.value}")
-
-
-map_tokens = {
-    TokenKind.Bond: map_bond,
-    TokenKind.Atom: map_atom,
-    TokenKind.Aromatic: map_atom,
-    TokenKind.AtomExtend: map_atom,
-    TokenKind.BranchStart: map_branch_start,
-    TokenKind.BranchEnd: map_branch_end,
-    TokenKind.Ring: map_ring,
-    TokenKind.Ring2: map_ring,
-    TokenKind.BondEZ: SkipSymbol,
-    TokenKind.Mix: NotImplementedFunc,
-    TokenKind.Rxn: NotImplementedFunc,
-    TokenKind.BondDescriptor: map_bond_descriptor,
-    TokenKind.StochasticSeperator: map_bond_seperator,
-    TokenKind.StochasticStart: map_stochastic_object_start,
-    TokenKind.StochasticEnd: map_stochastic_object_end,
-    TokenKind.ImplictEndGroup: map_bond_descriptor,
-    TokenKind.BondDescriptorLadder: NotImplementedFunc
-}
-
-
-def tokens_to_objects(constructor: BigSMILESStringConstructor, tokens: list[Token]) -> TokenKind | None:
-    """
-
-    Main loop for converting tokens into BigSMILES objects.
-
-    Parameters
-    ----------
-    constructor: BigSMILESStringConstructor
-
-    tokens:
-
-    Returns
-    -------
-
-    """
-    while tokens:
-        token = tokens.pop(0)
-
-        func = map_tokens[token.kind]
-        result = func(constructor, tokens, token)
-        if isinstance(result, TokenKind):
-            return result
-
-    constructor.run_validation()
-
-
-def parse_bigsmiles_str(input_text: str, bigsmiles):
-    """
-    Main function that turns BigSMILES string into a BigSMILES object.
-    Constructs BigSMILES tree in the provided object.
-    """
-    pre_validation(input_text)
-    tokens = tokenize(input_text)
-
-    with BigSMILESStringConstructor(bigsmiles) as constructor:
-        tokens_to_objects(constructor, tokens)
-        constructor.run_validation()
diff --git a/src/bigsmiles/reference_data/__init__.py b/src/bigsmiles/reference_data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bigsmiles/reference_data/chemical_data.py b/src/bigsmiles/reference_data/chemical_data.py
new file mode 100644
index 0000000..c406723
--- /dev/null
+++ b/src/bigsmiles/reference_data/chemical_data.py
@@ -0,0 +1,75 @@
+from __future__ import annotations
+import os
+import json
+
+
+bond_mapping = {
+    None: None,
+    ".": 0,
+    "": 1,
+    "-": 1,
+    "/": 1,
+    "\\": 1,
+    ":": 1.5,
+    "=": 2,
+    "#": 3,
+    "$": 4
+}
+
+stereo_bonds = {"/", "\\"}
+
+# load symbol data from JSON
+file_path = os.path.dirname(os.path.realpath(__file__))
+path_to_data = os.path.join(file_path, "elements_data.json")
+with open(path_to_data, "r", encoding="UTF-8") as f:
+    element_data = json.load(f)
+
+
+class Element:
+    __slots__ = ("name", "atomic_number", "atomic_mass", "period", "symbol", "electron_configuration", "group",
+                 "aromatic", "organics", "conflict", "valences")
+
+    def __init__(self,
+                 name: str,
+                 atomic_number: int,
+                 atomic_mass: float,
+                 period: int,
+                 symbol: str,
+                 electron_configuration: str,
+                 group: str,
+                 aromatic: bool,
+                 organics: bool,
+                 conflict: bool,
+                 valences: tuple[int]
+                 ):
+        self.name = name
+        self.atomic_number = atomic_number
+        self.atomic_mass = atomic_mass
+        self.period = period
+        self.symbol = symbol
+        self.electron_configuration = electron_configuration
+        self.group = group
+        self.aromatic = aromatic
+        self.organics = organics
+        self.conflict = conflict
+        self.valences = valences
+
+    def __str__(self):
+        return self.symbol
+
+    def __repr__(self):
+        return self.symbol
+
+
+periodic_table = tuple(Element(atomic_number=int(k), **v) for k, v in element_data.items())
+del element_data
+
+# create tuples, sets, and dictionaries for performance
+element_symbols = {element.symbol for element in periodic_table}
+elements_ordered = sorted(element_symbols, reverse=True)  # sorted in reverse so "Cl" hits before "C"
+aromatic_elements = {element.symbol.lower() for element in periodic_table if element.aromatic}
+elements_aromatic = element_symbols.union(aromatic_elements)
+organic_elements = {element.symbol for element in periodic_table if element.organics}
+organic_ordered = sorted(organic_elements, reverse=True)  # sorted in reverse so "Cl" hits before "C"
+atom_valences = {element.symbol: element.valences for element in periodic_table}
+symbol_to_atomic_number = {element.symbol: element.atomic_number for element in periodic_table}
diff --git a/src/bigsmiles/reference_data/elements_data.json b/src/bigsmiles/reference_data/elements_data.json
new file mode 100644
index 0000000..3cadbc4
--- /dev/null
+++ b/src/bigsmiles/reference_data/elements_data.json
@@ -0,0 +1,1677 @@
+{
+    "1": {
+        "name": "Hydrogen",
+        "atomic_mass": 1.008,
+        "period": 1,
+        "symbol": "H",
+        "electron_configuration": "1s1",
+        "group": 1,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            1
+        ]
+    },
+    "2": {
+        "name": "Helium",
+        "atomic_mass": 4.0026022,
+        "period": 1,
+        "symbol": "He",
+        "electron_configuration": "1s2",
+        "group": 18,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "3": {
+        "name": "Lithium",
+        "atomic_mass": 6.94,
+        "period": 2,
+        "symbol": "Li",
+        "electron_configuration": "1s2 2s1",
+        "group": 1,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            1
+        ]
+    },
+    "4": {
+        "name": "Beryllium",
+        "atomic_mass": 9.01218315,
+        "period": 2,
+        "symbol": "Be",
+        "electron_configuration": "1s2 2s2",
+        "group": 2,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            2
+        ]
+    },
+    "5": {
+        "name": "Boron",
+        "atomic_mass": 10.81,
+        "period": 2,
+        "symbol": "B",
+        "electron_configuration": "1s2 2s2 2p1",
+        "group": 13,
+        "aromatic": true,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            3
+        ]
+    },
+    "6": {
+        "name": "Carbon",
+        "atomic_mass": 12.011,
+        "period": 2,
+        "symbol": "C",
+        "electron_configuration": "1s2 2s2 2p2",
+        "group": 14,
+        "aromatic": true,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            4
+        ]
+    },
+    "7": {
+        "name": "Nitrogen",
+        "atomic_mass": 14.007,
+        "period": 2,
+        "symbol": "N",
+        "electron_configuration": "1s2 2s2 2p3",
+        "group": 15,
+        "aromatic": true,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            3
+        ]
+    },
+    "8": {
+        "name": "Oxygen",
+        "atomic_mass": 15.999,
+        "period": 2,
+        "symbol": "O",
+        "electron_configuration": "1s2 2s2 2p4",
+        "group": 16,
+        "aromatic": true,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            2
+        ]
+    },
+    "9": {
+        "name": "Fluorine",
+        "atomic_mass": 18.9984031636,
+        "period": 2,
+        "symbol": "F",
+        "electron_configuration": "1s2 2s2 2p5",
+        "group": 17,
+        "aromatic": false,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            1
+        ]
+    },
+    "10": {
+        "name": "Neon",
+        "atomic_mass": 20.17976,
+        "period": 2,
+        "symbol": "Ne",
+        "electron_configuration": "1s2 2s2 2p6",
+        "group": 18,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "11": {
+        "name": "Sodium",
+        "atomic_mass": 22.989769282,
+        "period": 3,
+        "symbol": "Na",
+        "electron_configuration": "1s2 2s2 2p6 3s1",
+        "group": 1,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            1
+        ]
+    },
+    "12": {
+        "name": "Magnesium",
+        "atomic_mass": 24.305,
+        "period": 3,
+        "symbol": "Mg",
+        "electron_configuration": "1s2 2s2 2p6 3s2",
+        "group": 2,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            2
+        ]
+    },
+    "13": {
+        "name": "Aluminium",
+        "atomic_mass": 26.98153857,
+        "period": 3,
+        "symbol": "Al",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p1",
+        "group": 13,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "14": {
+        "name": "Silicon",
+        "atomic_mass": 28.085,
+        "period": 3,
+        "symbol": "Si",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p2",
+        "group": 14,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            4
+        ]
+    },
+    "15": {
+        "name": "Phosphorus",
+        "atomic_mass": 30.9737619985,
+        "period": 3,
+        "symbol": "P",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p3",
+        "group": 15,
+        "aromatic": true,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            3,
+            5
+        ]
+    },
+    "16": {
+        "name": "Sulfur",
+        "atomic_mass": 32.06,
+        "period": 3,
+        "symbol": "S",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p4",
+        "group": 16,
+        "aromatic": true,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            2,
+            4,
+            6
+        ]
+    },
+    "17": {
+        "name": "Chlorine",
+        "atomic_mass": 35.45,
+        "period": 3,
+        "symbol": "Cl",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p5",
+        "group": 17,
+        "aromatic": false,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            1,
+            3,
+            5,
+            7
+        ]
+    },
+    "18": {
+        "name": "Argon",
+        "atomic_mass": 39.9481,
+        "period": 3,
+        "symbol": "Ar",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6",
+        "group": 18,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "19": {
+        "name": "Potassium",
+        "atomic_mass": 39.09831,
+        "period": 4,
+        "symbol": "K",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s1",
+        "group": 1,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            1
+        ]
+    },
+    "20": {
+        "name": "Calcium",
+        "atomic_mass": 40.0784,
+        "period": 4,
+        "symbol": "Ca",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2",
+        "group": 2,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            2
+        ]
+    },
+    "21": {
+        "name": "Scandium",
+        "atomic_mass": 44.9559085,
+        "period": 4,
+        "symbol": "Sc",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d1",
+        "group": 3,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "22": {
+        "name": "Titanium",
+        "atomic_mass": 47.8671,
+        "period": 4,
+        "symbol": "Ti",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d2",
+        "group": 4,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            3
+        ]
+    },
+    "23": {
+        "name": "Vanadium",
+        "atomic_mass": 50.94151,
+        "period": 4,
+        "symbol": "V",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d3",
+        "group": 5,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "24": {
+        "name": "Chromium",
+        "atomic_mass": 51.99616,
+        "period": 4,
+        "symbol": "Cr",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s1 3d5",
+        "group": 6,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "25": {
+        "name": "Manganese",
+        "atomic_mass": 54.9380443,
+        "period": 4,
+        "symbol": "Mn",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d5",
+        "group": 7,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "26": {
+        "name": "Iron",
+        "atomic_mass": 55.8452,
+        "period": 4,
+        "symbol": "Fe",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d6",
+        "group": 8,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "27": {
+        "name": "Cobalt",
+        "atomic_mass": 58.9331944,
+        "period": 4,
+        "symbol": "Co",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d7",
+        "group": 9,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            8
+        ]
+    },
+    "28": {
+        "name": "Nickel",
+        "atomic_mass": 58.69344,
+        "period": 4,
+        "symbol": "Ni",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d8",
+        "group": 10,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "29": {
+        "name": "Copper",
+        "atomic_mass": 63.5463,
+        "period": 4,
+        "symbol": "Cu",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s1 3d10",
+        "group": 11,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "30": {
+        "name": "Zinc",
+        "atomic_mass": 65.382,
+        "period": 4,
+        "symbol": "Zn",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10",
+        "group": 12,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "31": {
+        "name": "Gallium",
+        "atomic_mass": 69.7231,
+        "period": 4,
+        "symbol": "Ga",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p1",
+        "group": 13,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            3
+        ]
+    },
+    "32": {
+        "name": "Germanium",
+        "atomic_mass": 72.6308,
+        "period": 4,
+        "symbol": "Ge",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p2",
+        "group": 14,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            4
+        ]
+    },
+    "33": {
+        "name": "Arsenic",
+        "atomic_mass": 74.9215956,
+        "period": 4,
+        "symbol": "As",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p3",
+        "group": 15,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            3,
+            5
+        ]
+    },
+    "34": {
+        "name": "Selenium",
+        "atomic_mass": 78.9718,
+        "period": 4,
+        "symbol": "Se",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p4",
+        "group": 16,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            2,
+            4,
+            6
+        ]
+    },
+    "35": {
+        "name": "Bromine",
+        "atomic_mass": 79.904,
+        "period": 4,
+        "symbol": "Br",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p5",
+        "group": 17,
+        "aromatic": false,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            1
+        ]
+    },
+    "36": {
+        "name": "Krypton",
+        "atomic_mass": 83.7982,
+        "period": 4,
+        "symbol": "Kr",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6",
+        "group": 18,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "37": {
+        "name": "Rubidium",
+        "atomic_mass": 85.46783,
+        "period": 5,
+        "symbol": "Rb",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s1",
+        "group": 1,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            1
+        ]
+    },
+    "38": {
+        "name": "Strontium",
+        "atomic_mass": 87.621,
+        "period": 5,
+        "symbol": "Sr",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2",
+        "group": 2,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            2
+        ]
+    },
+    "39": {
+        "name": "Yttrium",
+        "atomic_mass": 88.905842,
+        "period": 5,
+        "symbol": "Y",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d1",
+        "group": 3,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "40": {
+        "name": "Zirconium",
+        "atomic_mass": 91.2242,
+        "period": 5,
+        "symbol": "Zr",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d2",
+        "group": 4,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "41": {
+        "name": "Niobium",
+        "atomic_mass": 92.906372,
+        "period": 5,
+        "symbol": "Nb",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s1 4d4",
+        "group": 5,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            8
+        ]
+    },
+    "42": {
+        "name": "Molybdenum",
+        "atomic_mass": 95.951,
+        "period": 5,
+        "symbol": "Mo",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s1 4d5",
+        "group": 6,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "43": {
+        "name": "Technetium",
+        "atomic_mass": 98,
+        "period": 5,
+        "symbol": "Tc",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d5",
+        "group": 7,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "44": {
+        "name": "Ruthenium",
+        "atomic_mass": 101.072,
+        "period": 5,
+        "symbol": "Ru",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s1 4d7",
+        "group": 8,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "45": {
+        "name": "Rhodium",
+        "atomic_mass": 102.905502,
+        "period": 5,
+        "symbol": "Rh",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s1 4d8",
+        "group": 9,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "46": {
+        "name": "Palladium",
+        "atomic_mass": 106.421,
+        "period": 5,
+        "symbol": "Pd",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 4d10",
+        "group": 10,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "47": {
+        "name": "Silver",
+        "atomic_mass": 107.86822,
+        "period": 5,
+        "symbol": "Ag",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s1 4d10",
+        "group": 11,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "48": {
+        "name": "Cadmium",
+        "atomic_mass": 112.4144,
+        "period": 5,
+        "symbol": "Cd",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10",
+        "group": 12,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "49": {
+        "name": "Indium",
+        "atomic_mass": 114.8181,
+        "period": 5,
+        "symbol": "In",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p1",
+        "group": 13,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            3
+        ]
+    },
+    "50": {
+        "name": "Tin",
+        "atomic_mass": 118.7107,
+        "period": 5,
+        "symbol": "Sn",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p2",
+        "group": 14,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            2,
+            4
+        ]
+    },
+    "51": {
+        "name": "Antimony",
+        "atomic_mass": 121.7601,
+        "period": 5,
+        "symbol": "Sb",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p3",
+        "group": 15,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            3,
+            5
+        ]
+    },
+    "52": {
+        "name": "Tellurium",
+        "atomic_mass": 127.603,
+        "period": 5,
+        "symbol": "Te",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p4",
+        "group": 16,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            2,
+            4,
+            6
+        ]
+    },
+    "53": {
+        "name": "Iodine",
+        "atomic_mass": 126.904473,
+        "period": 5,
+        "symbol": "I",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p5",
+        "group": 17,
+        "aromatic": false,
+        "organics": true,
+        "conflict": false,
+        "valences": [
+            1,
+            3,
+            5,
+            7
+        ]
+    },
+    "54": {
+        "name": "Xenon",
+        "atomic_mass": 131.2936,
+        "period": 5,
+        "symbol": "Xe",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6",
+        "group": 18,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "55": {
+        "name": "Cesium",
+        "atomic_mass": 132.905451966,
+        "period": 6,
+        "symbol": "Cs",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s1",
+        "group": 1,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            1
+        ]
+    },
+    "56": {
+        "name": "Barium",
+        "atomic_mass": 137.3277,
+        "period": 6,
+        "symbol": "Ba",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2",
+        "group": 2,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            2
+        ]
+    },
+    "57": {
+        "name": "Lanthanum",
+        "atomic_mass": 138.905477,
+        "period": 6,
+        "symbol": "La",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 5d1",
+        "group": 3,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "58": {
+        "name": "Cerium",
+        "atomic_mass": 140.1161,
+        "period": 6,
+        "symbol": "Ce",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 5d1 4f1",
+        "group": 4,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "59": {
+        "name": "Praseodymium",
+        "atomic_mass": 140.907662,
+        "period": 6,
+        "symbol": "Pr",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f3",
+        "group": 5,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "60": {
+        "name": "Neodymium",
+        "atomic_mass": 144.2423,
+        "period": 6,
+        "symbol": "Nd",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f4",
+        "group": 6,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "61": {
+        "name": "Promethium",
+        "atomic_mass": 145,
+        "period": 6,
+        "symbol": "Pm",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f5",
+        "group": 7,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "62": {
+        "name": "Samarium",
+        "atomic_mass": 150.362,
+        "period": 6,
+        "symbol": "Sm",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f6",
+        "group": 8,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "63": {
+        "name": "Europium",
+        "atomic_mass": 151.9641,
+        "period": 6,
+        "symbol": "Eu",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f7",
+        "group": 9,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "64": {
+        "name": "Gadolinium",
+        "atomic_mass": 157.253,
+        "period": 6,
+        "symbol": "Gd",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f7 5d1",
+        "group": 10,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "65": {
+        "name": "Terbium",
+        "atomic_mass": 158.925352,
+        "period": 6,
+        "symbol": "Tb",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f9",
+        "group": 11,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "66": {
+        "name": "Dysprosium",
+        "atomic_mass": 162.5001,
+        "period": 6,
+        "symbol": "Dy",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f10",
+        "group": 12,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "67": {
+        "name": "Holmium",
+        "atomic_mass": 164.930332,
+        "period": 6,
+        "symbol": "Ho",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f11",
+        "group": 13,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            8
+        ]
+    },
+    "68": {
+        "name": "Erbium",
+        "atomic_mass": 167.2593,
+        "period": 6,
+        "symbol": "Er",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f12",
+        "group": 14,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "69": {
+        "name": "Thulium",
+        "atomic_mass": 168.934222,
+        "period": 6,
+        "symbol": "Tm",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f13",
+        "group": 15,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "70": {
+        "name": "Ytterbium",
+        "atomic_mass": 173.0451,
+        "period": 6,
+        "symbol": "Yb",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14",
+        "group": 16,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            8
+        ]
+    },
+    "71": {
+        "name": "Lutetium",
+        "atomic_mass": 174.96681,
+        "period": 6,
+        "symbol": "Lu",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d1",
+        "group": 17,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "72": {
+        "name": "Hafnium",
+        "atomic_mass": 178.492,
+        "period": 6,
+        "symbol": "Hf",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d2",
+        "group": 4,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "73": {
+        "name": "Tantalum",
+        "atomic_mass": 180.947882,
+        "period": 6,
+        "symbol": "Ta",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d3",
+        "group": 5,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "74": {
+        "name": "Tungsten",
+        "atomic_mass": 183.841,
+        "period": 6,
+        "symbol": "W",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d4",
+        "group": 6,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "75": {
+        "name": "Rhenium",
+        "atomic_mass": 186.2071,
+        "period": 6,
+        "symbol": "Re",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d5",
+        "group": 7,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "76": {
+        "name": "Osmium",
+        "atomic_mass": 190.233,
+        "period": 6,
+        "symbol": "Os",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d6",
+        "group": 8,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            8
+        ]
+    },
+    "77": {
+        "name": "Iridium",
+        "atomic_mass": 192.2173,
+        "period": 6,
+        "symbol": "Ir",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d7",
+        "group": 9,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "78": {
+        "name": "Platinum",
+        "atomic_mass": 195.0849,
+        "period": 6,
+        "symbol": "Pt",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s1 4f14 5d9",
+        "group": 10,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "79": {
+        "name": "Gold",
+        "atomic_mass": 196.9665695,
+        "period": 6,
+        "symbol": "Au",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s1 4f14 5d10",
+        "group": 11,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "80": {
+        "name": "Mercury",
+        "atomic_mass": 200.5923,
+        "period": 6,
+        "symbol": "Hg",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10",
+        "group": 12,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "81": {
+        "name": "Thallium",
+        "atomic_mass": 204.38,
+        "period": 6,
+        "symbol": "Tl",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p1",
+        "group": 13,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "82": {
+        "name": "Lead",
+        "atomic_mass": 207.21,
+        "period": 6,
+        "symbol": "Pb",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p2",
+        "group": 14,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            2,
+            4
+        ]
+    },
+    "83": {
+        "name": "Bismuth",
+        "atomic_mass": 208.980401,
+        "period": 6,
+        "symbol": "Bi",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p3",
+        "group": 15,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            3,
+            5
+        ]
+    },
+    "84": {
+        "name": "Polonium",
+        "atomic_mass": 209,
+        "period": 6,
+        "symbol": "Po",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p4",
+        "group": 16,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            2,
+            4,
+            6
+        ]
+    },
+    "85": {
+        "name": "Astatine",
+        "atomic_mass": 210,
+        "period": 6,
+        "symbol": "At",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p5",
+        "group": 17,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            1,
+            3,
+            5,
+            7
+        ]
+    },
+    "86": {
+        "name": "Radon",
+        "atomic_mass": 222,
+        "period": 6,
+        "symbol": "Rn",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6",
+        "group": 18,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "87": {
+        "name": "Francium",
+        "atomic_mass": 223,
+        "period": 7,
+        "symbol": "Fr",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s1",
+        "group": 1,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            1
+        ]
+    },
+    "88": {
+        "name": "Radium",
+        "atomic_mass": 226,
+        "period": 7,
+        "symbol": "Ra",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2",
+        "group": 2,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            2
+        ]
+    },
+    "89": {
+        "name": "Actinium",
+        "atomic_mass": 227,
+        "period": 7,
+        "symbol": "Ac",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 6d1",
+        "group": 3,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "90": {
+        "name": "Thorium",
+        "atomic_mass": 232.03774,
+        "period": 7,
+        "symbol": "Th",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 6d2",
+        "group": 4,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "91": {
+        "name": "Protactinium",
+        "atomic_mass": 231.035882,
+        "period": 7,
+        "symbol": "Pa",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f2 6d1",
+        "group": 5,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "92": {
+        "name": "Uranium",
+        "atomic_mass": 238.028913,
+        "period": 7,
+        "symbol": "U",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f3 6d1",
+        "group": 6,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "93": {
+        "name": "Neptunium",
+        "atomic_mass": 237,
+        "period": 7,
+        "symbol": "Np",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f4 6d1",
+        "group": 7,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            8
+        ]
+    },
+    "94": {
+        "name": "Plutonium",
+        "atomic_mass": 244,
+        "period": 7,
+        "symbol": "Pu",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f6",
+        "group": 8,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "95": {
+        "name": "Americium",
+        "atomic_mass": 243,
+        "period": 7,
+        "symbol": "Am",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f7",
+        "group": 9,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "96": {
+        "name": "Curium",
+        "atomic_mass": 247,
+        "period": 7,
+        "symbol": "Cm",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f7 6d1",
+        "group": 10,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "97": {
+        "name": "Berkelium",
+        "atomic_mass": 247,
+        "period": 7,
+        "symbol": "Bk",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f9",
+        "group": 11,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "98": {
+        "name": "Californium",
+        "atomic_mass": 251,
+        "period": 7,
+        "symbol": "Cf",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f10",
+        "group": 12,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "99": {
+        "name": "Einsteinium",
+        "atomic_mass": 252,
+        "period": 7,
+        "symbol": "Es",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f11",
+        "group": 13,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "100": {
+        "name": "Fermium",
+        "atomic_mass": 257,
+        "period": 7,
+        "symbol": "Fm",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f12",
+        "group": 14,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "101": {
+        "name": "Mendelevium",
+        "atomic_mass": 258,
+        "period": 7,
+        "symbol": "Md",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f13",
+        "group": 15,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "102": {
+        "name": "Nobelium",
+        "atomic_mass": 259,
+        "period": 7,
+        "symbol": "No",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14",
+        "group": 16,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            8
+        ]
+    },
+    "103": {
+        "name": "Lawrencium",
+        "atomic_mass": 266,
+        "period": 7,
+        "symbol": "Lr",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 7p1",
+        "group": 17,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "104": {
+        "name": "Rutherfordium",
+        "atomic_mass": 267,
+        "period": 7,
+        "symbol": "Rf",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d2",
+        "group": 4,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "105": {
+        "name": "Dubnium",
+        "atomic_mass": 268,
+        "period": 7,
+        "symbol": "Db",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d3",
+        "group": 5,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "106": {
+        "name": "Seaborgium",
+        "atomic_mass": 269,
+        "period": 7,
+        "symbol": "Sg",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d4",
+        "group": 6,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "107": {
+        "name": "Bohrium",
+        "atomic_mass": 270,
+        "period": 7,
+        "symbol": "Bh",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d5",
+        "group": 7,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "108": {
+        "name": "Hassium",
+        "atomic_mass": 269,
+        "period": 7,
+        "symbol": "Hs",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d6",
+        "group": 8,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "109": {
+        "name": "Meitnerium",
+        "atomic_mass": 278,
+        "period": 7,
+        "symbol": "Mt",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d7",
+        "group": 9,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "110": {
+        "name": "Darmstadtium",
+        "atomic_mass": 281,
+        "period": 7,
+        "symbol": "Ds",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d8",
+        "group": 10,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "111": {
+        "name": "Roentgenium",
+        "atomic_mass": 282,
+        "period": 7,
+        "symbol": "Rg",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d9",
+        "group": 11,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "112": {
+        "name": "Copernicium",
+        "atomic_mass": 285,
+        "period": 7,
+        "symbol": "Cn",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10",
+        "group": 12,
+        "aromatic": false,
+        "organics": false,
+        "conflict": true,
+        "valences": [
+            8
+        ]
+    },
+    "113": {
+        "name": "Nihonium",
+        "atomic_mass": 286,
+        "period": 7,
+        "symbol": "Nh",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10 7p1",
+        "group": 13,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "114": {
+        "name": "Flerovium",
+        "atomic_mass": 289,
+        "period": 7,
+        "symbol": "Fl",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10 7p2",
+        "group": 14,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "115": {
+        "name": "Moscovium",
+        "atomic_mass": 289,
+        "period": 7,
+        "symbol": "Mc",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10 7p3",
+        "group": 15,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "116": {
+        "name": "Livermorium",
+        "atomic_mass": 293,
+        "period": 7,
+        "symbol": "Lv",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10 7p4",
+        "group": 16,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "117": {
+        "name": "Tennessine",
+        "atomic_mass": 294,
+        "period": 7,
+        "symbol": "Ts",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10 7p5",
+        "group": 17,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    },
+    "118": {
+        "name": "Oganesson",
+        "atomic_mass": 294,
+        "period": 7,
+        "symbol": "Og",
+        "electron_configuration": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10 7p6",
+        "group": 18,
+        "aromatic": false,
+        "organics": false,
+        "conflict": false,
+        "valences": [
+            8
+        ]
+    }
+}
\ No newline at end of file
diff --git a/src/bigsmiles/syntax_fixes.py b/src/bigsmiles/syntax_fixes.py
deleted file mode 100644
index 32f3a2a..0000000
--- a/src/bigsmiles/syntax_fixes.py
+++ /dev/null
@@ -1,48 +0,0 @@
-
-from bigsmiles.bigsmiles import BigSMILES, Branch, StochasticObject, StochasticFragment
-
-
-def run_syntax_fixes(bigsmiles: BigSMILES):
-    remove_unnecessary_branch_symbols(bigsmiles)
-    check_ring_numbers(bigsmiles)
-
-
-def remove_unnecessary_branch_symbols(obj: BigSMILES | StochasticObject | StochasticFragment | Branch):
-    # check if branch is notation is need; if not needed remove it and re-do ids
-    # Example: CCC(C(CC)) --> CCCCCC
-
-    for node in obj.nodes:
-        if hasattr(node, "nodes"):
-            remove_unnecessary_branch_symbols(node)
-
-    branch_check(obj)
-
-
-def branch_check(obj: StochasticObject | StochasticFragment | Branch):
-    # checks if branch is end of nodes; if it is then it's not needed
-    if isinstance(obj.nodes[-1], Branch):
-        branch = obj.nodes.pop()
-        obj.nodes += branch.nodes
-
-        if hasattr(obj, 'parent'):
-            renumber_branches(obj.parent)
-        else:
-            renumber_branches(obj)
-
-
-def renumber_branches(obj, counter: int = 0) -> int:
-    # Recursive function: loops through obj and re-numbers branch.id_
-    for node in obj.nodes:
-        if isinstance(node, Branch):
-            node.id_ = counter
-            counter += 1
-        if hasattr(node, "nodes"):
-            counter = renumber_branches(node, counter)
-
-    return counter
-
-
-def check_ring_numbers(obj: BigSMILES):
-    # check if ring_counter had been skipped and restart everything at 1
-    for i, ring in enumerate(obj.rings):
-        ring.ring_id = i + 1
diff --git a/src/bigsmiles/tokenizer.py b/src/bigsmiles/tokenizer.py
deleted file mode 100644
index 3f1e664..0000000
--- a/src/bigsmiles/tokenizer.py
+++ /dev/null
@@ -1,123 +0,0 @@
-"""
-
-Code for tokenizing a BigSMILES string.
-
-"""
-
-import enum
-import re
-
-from bigsmiles.config import Config
-
-
-class BigSMILESTokenizeError(Exception):
-    """ Raised when error tokenizing a BigSMILES string. """
-    def __init__(self, text: str):
-        self.text = text
-
-    def __str__(self):
-        return self.text
-
-
-class TokenKind(enum.Enum):
-    Bond = 0
-    Atom = 1
-    Aromatic = 2
-    AtomExtend = 3
-    BranchStart = 4
-    BranchEnd = 5
-    Ring = 6
-    Ring2 = 7
-    BondEZ = 8
-    Mix = 9
-    Rxn = 10
-    BondDescriptor = 11
-    StochasticSeperator = 12
-    StochasticStart = 13
-    StochasticEnd = 14
-    ImplictEndGroup = 15
-    BondDescriptorLadder = 16
-
-
-_isotope_pattern = r'(?P<isotope>[\d]{1,3})?'
-_element_pattern = r'(?P<element>' + "|".join(Config.elements_ordered) + "|".join(Config.aromatic) + '{1})'
-_stereo_pattern = r'(?P<stereo>@{1,2})?'
-_hydrogen_pattern = r'(?P<hcount>H[\d]?)?'
-_charge_pattern = r'(?P<charge>[-|\+]{1,3}[\d]?)?'
-atom_pattern = r"(?:\[)" + _isotope_pattern + _element_pattern + _stereo_pattern + \
-               _hydrogen_pattern + _charge_pattern + r"(?:\])"
-
-token_specification = [
-    # order in the list is important; regex stops at first match
-    (TokenKind.Bond.name, r'[=|#]'),
-    (TokenKind.Atom.name, "|".join(Config.elements_ordered)),
-    (TokenKind.Aromatic.name, "|".join(Config.aromatic)),
-    (TokenKind.AtomExtend.name, atom_pattern),  # Atom in brackets
-    (TokenKind.BranchStart.name, r'\('),
-    (TokenKind.BranchEnd.name, r'\)'),
-    (TokenKind.Ring.name, r'[\d]{1}'),
-    (TokenKind.Ring2.name, r'%[\d]{2}'),  # ring_id with two-digit numbers
-    (TokenKind.BondEZ.name, r'/|\\'),  # cis trans
-    (TokenKind.Mix.name, r"\."),  # mixture
-    (TokenKind.Rxn.name, r">>|>"),  # reaction -->
-
-    (TokenKind.BondDescriptorLadder.name, r"\[[$<>][\d]\[[$<>][\d]?\][\d]?\]"),  # Ladder
-    (TokenKind.BondDescriptor.name, r"\[[$<>][\d]?[\d]?\]"),
-    (TokenKind.StochasticSeperator.name, r",|;"),
-    (TokenKind.StochasticStart.name, r'\{'),
-    (TokenKind.StochasticEnd.name, r'\}'),
-    (TokenKind.ImplictEndGroup.name, r'\[\]'),
-
-    ('SKIP', r'[ \t]+'),  # Skip over spaces and tabs
-    ('MISMATCH', r'.'),  # Any other character
-]
-
-tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
-
-tok_regex = re.compile(tok_regex)
-
-
-class Token:
-    __slots__ = ("kind", "value")
-
-    def __init__(self, kind: TokenKind, value: str):
-        self.kind = kind
-        self.value = value
-
-    def __str__(self):
-        return f"{self.kind}: {self.value}"
-
-
-def tokenize(text: str) -> list[Token]:
-    """
-
-    Tokenizes a bigSMILES string.
-
-    Parameters
-    ----------
-    text: str
-        BigSMILES string
-
-    Returns
-    -------
-    result: list[Token]
-        BigSMILES as a token list
-
-    """
-    result = []
-    for match in re.finditer(tok_regex, text.strip()):
-        kind = match.lastgroup
-
-        value = match.group()
-        if kind == 'SKIP':
-            continue
-        elif kind == 'MISMATCH':
-            raise BigSMILESTokenizeError(f'Invalid symbol (or group of symbols). (starting with {value!r}; '
-                                         f'index: {match.span()[0]})'
-                                         f'\n{text}' + "\n" + " " * match.span()[0] + "^(and forward)")
-
-        result.append(
-            Token(TokenKind[kind], value)
-        )
-
-    return result
diff --git a/src/bigsmiles/validation.py b/src/bigsmiles/validation.py
deleted file mode 100644
index cf380da..0000000
--- a/src/bigsmiles/validation.py
+++ /dev/null
@@ -1,146 +0,0 @@
-"""
-
-Remaining validation done on the bigsmile object
-
-Note:
-* tokenization should take care of symbol validation
-* Bigsmiles_constructor should take care of most bond and valance issues
-
-This is mainly to catch additional leftover validation.
-
-"""
-
-from bigsmiles.errors import BigSMILESError
-from bigsmiles.bigsmiles import BigSMILES, StochasticObject, StochasticFragment, \
-    BondDescriptor, Branch
-
-
-## Pre-construction validation#############################################################################
-def pre_validation(text: str):
-    """ Main entry point for pre-construction validation. """
-    branch_symbol_validation(text)
-    stochastic_object_validation(text)
-    brackets_validation(text)
-
-
-def branch_symbol_validation(text: str):
-    count = text.count('(') - text.count(')')
-    if count != 0:
-        if count > 0:
-            raise BigSMILESError(f"Invalid BigSMILES. Missing {count} closing branch symbols ')'. "
-                                 f"\n Invalid string: {text}")
-        raise BigSMILESError(f"Invalid BigSMILES. Missing {count} opening branch symbols '('. "
-                             f"\n Invalid string: {text}")
-
-
-def stochastic_object_validation(text: str):
-    count = text.count('{') - text.count('}')
-    if count != 0:
-        if count > 0:
-            raise BigSMILESError(f"Invalid BigSMILES. Missing {count} closing stochastic object symbols" + " '}'. "
-                                 f"\n Invalid string: {text}")
-        raise BigSMILESError(f"Invalid BigSMILES. Missing {count} opening stochastic object symbols " + "'{'. "
-                             f"\n Invalid string: {text}")
-
-
-def brackets_validation(text: str):
-    count = text.count('[') - text.count(']')
-    if count != 0:
-        if count > 0:
-            raise BigSMILESError(f"Invalid BigSMILES. Missing {count} closing bracket symbols ']'. "
-                                 f"\n Invalid string: {text}")
-        raise BigSMILESError(f"Invalid BigSMILES. Missing {count} opening bracket symbols '['. "
-                             f"\n Invalid string: {text}")
-
-
-## Post-construction validation#############################################################################
-def post_validation(bigsmiles: BigSMILES):
-    """ Main entry point for post-construction validation. """
-    check_ring_closure(bigsmiles)
-    check_bonding_descriptors(bigsmiles)
-    check_implicit_endgroups_ends(bigsmiles)
-
-
-def check_ring_closure(bigsmiles: BigSMILES):
-    """ Checks to see that all rings have been closed that were started. """
-    for ring in bigsmiles.rings:
-        if ring.atom2 is None:
-            raise BigSMILESError(f"Ring opened, but not closed. (ring id: {ring.ring_id})")
-
-
-def check_bonding_descriptors(bigsmiles: BigSMILES | StochasticObject | Branch):
-    """
-
-    rules:
-    if [$] must be 2 or more
-    if [>] there must be [<]
-    if [<] there must be [>]
-    if end is implicit; there must be single bonding units
-    if ends are explict they must both have a matching pair in the stochastic fragments
-
-
-
-    Parameters
-    ----------
-    bigsmiles
-
-    Returns
-    -------
-
-    """
-    for node in bigsmiles.nodes:
-        if isinstance(node, StochasticObject):
-            do_check_bonding_descriptors(node)
-
-            # check nested stochastic objects
-            check_bonding_descriptors(node)
-        if isinstance(node, Branch):
-            check_bonding_descriptors(node)
-
-
-def do_check_bonding_descriptors(stoch_obj: StochasticObject):
-    for bd in stoch_obj.bonding_descriptors:
-        # if [$], [$0], [$1], ... must be 2 or more
-        if bd.descriptor is "$":
-            if len(bd.instances) <= 1:
-                raise BigSMILESError("[$] type bonding descriptors require more than one instances in a string.")
-
-        # if [>] there must be [<] and if [<] there must be [>]
-        if bd.descriptor in ("<", ">"):
-            bd_pair = find_complementing_bonding_descriptor(stoch_obj, bd)
-            if bd_pair is None:
-                raise BigSMILESError(f'{bd} complementary partner not found.')
-
-
-def find_complementing_bonding_descriptor(stoch_obj: StochasticObject, bond_descr: BondDescriptor) \
-        -> BondDescriptor | None:
-    for bd in stoch_obj.bonding_descriptors:
-        if bond_descr.is_pair(bd):
-            return bd
-
-    return None
-
-
-def check_implicit_endgroups_ends(obj: BigSMILES | StochasticObject | StochasticFragment | Branch,
-                                  parent_obj: BigSMILES | StochasticObject | StochasticFragment | Branch = None):
-    # if end is implicit; there must be single bonding units
-    for i, node in enumerate(obj.nodes):
-        if isinstance(node, StochasticObject):
-            if node.end_group_left.descriptor.descriptor is "":
-                if i != 0:
-                    # nothing allowed to the left
-                    raise BigSMILESError("With a the left end group implicit, "
-                                         "there should be nothing to the left of the stochastic object.")
-                if parent_obj is not None:
-                    # if it isn't BigSMILES it can't have a left implicit end group
-                    raise BigSMILESError("Implicit left end group not allowed within interior.")
-
-            if node.end_group_right.descriptor.descriptor is "":
-
-                if i != len(obj.nodes) - 1:
-                    # nothing allowed to the right
-                    raise BigSMILESError("With a implicit right end group, there should not be anything to the "
-                                         "right of it.")
-
-        if hasattr(node, "nodes"):
-            check_implicit_endgroups_ends(node, obj)
diff --git a/src/bigsmiles/validation/__init__.py b/src/bigsmiles/validation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bigsmiles/validation/validation_bigsmiles_obj.py b/src/bigsmiles/validation/validation_bigsmiles_obj.py
new file mode 100644
index 0000000..417cedb
--- /dev/null
+++ b/src/bigsmiles/validation/validation_bigsmiles_obj.py
@@ -0,0 +1,162 @@
+"""
+
+Remaining validation done on the BigSMILES object
+
+Note:
+* tokenization should take care of symbol validation
+* Bigsmiles_constructor should take care of most bond and valence issues
+
+This is mainly to catch additional leftover validation.
+
+"""
+from __future__ import annotations
+
+from bigsmiles.data_structures.bigsmiles import BigSMILES, StochasticObject, Branch, BondDescriptor, Bond, has_node_attr
+from bigsmiles.errors import ValidationError
+
+
+def run_syntax_fixes(bigsmiles: BigSMILES):
+    remove_unnecessary_branch_symbols(bigsmiles)
+    check_ring_numbers(bigsmiles)
+
+
+def remove_unnecessary_branch_symbols(obj: has_node_attr):
+    """
+    Checks if branch is notation is need; if not needed remove it
+    Example: CCC(C(CC)) --> CCCCCC
+    recursive function
+    """
+    for node in obj.nodes:
+        # recursive call
+        if hasattr(node, "nodes"):
+            remove_unnecessary_branch_symbols(node)
+
+    # do check
+    if isinstance(obj.nodes[-1], Branch):
+        # checks if branch is end of nodes; if it is then it's not needed
+        branch = obj.nodes.pop()
+        obj.nodes += branch.nodes
+
+
+def check_ring_numbers(obj: has_node_attr):
+    """ Renumber rings. """
+    rings = _get_rings(obj, [])
+
+    if not rings:
+        return
+
+    for i, ring in enumerate(rings):
+        ring.ring_id = i + 1
+
+
+def _get_rings(obj: has_node_attr, ring_list: list[Bond]) -> list[Bond]:
+    """ Recursive get rings. """
+    for node in obj.nodes:
+        if hasattr(node, 'nodes'):
+            _get_rings(node, ring_list)
+
+    if hasattr(obj, 'rings'):
+        ring_list += obj.rings
+    return ring_list
+
+
+def re_number_node_ids(obj: has_node_attr, id_: int = 0):
+    """ Recursive renumbering 'id_'. """
+    for node in obj.nodes:
+        node.id_ = id_
+        id_ += 1
+        if hasattr(node, 'nodes'):
+            re_number_node_ids(node, id_)
+
+
+## Error checks ## noqa
+#######################################################################################################################
+def run_validation(bigsmiles: BigSMILES):
+    """ Main entry point for post-construction validation. """
+    check_ring_closure(bigsmiles)
+    check_bonding_descriptors(bigsmiles)
+    check_implicit_endgroups_ends(bigsmiles)
+
+
+def check_ring_closure(bigsmiles: BigSMILES):
+    """ Checks to see that all rings have been closed that were started. """
+    for ring in bigsmiles.rings:
+        if ring.atom2 is None:
+            raise ValidationError(f"Ring opened, but not closed. (ring id: {ring.ring_id})")
+
+
+def check_bonding_descriptors(bigsmiles: BigSMILES | StochasticObject | Branch):
+    """
+
+    rules:
+    if [>] there must be [<]
+    if [<] there must be [>]
+    if end is implicit; there must be single bonding units
+    if ends are explict they must both have a matching pair in the stochastic fragments
+
+
+
+    Parameters
+    ----------
+    bigsmiles
+
+    Returns
+    -------
+
+    """
+    for node in bigsmiles.nodes:
+        if isinstance(node, StochasticObject):
+            _do_check_bonding_descriptors(node)
+
+        # check nested stochastic objects
+        if hasattr(node, "nodes"):
+            check_bonding_descriptors(node)
+
+
+def _do_check_bonding_descriptors(stoch_obj: StochasticObject):
+    for bd in stoch_obj.bonding_descriptors:
+        _ = bd.bond_symbol  # runs bond order check
+
+        # if [$], [$0], [$1], ... must be 2 or more
+        if bd.descriptor == "$":
+            if len(bd.instances) < 1:
+                raise ValidationError("[$] type bonding descriptors require more than one instances in a string.")
+
+        # if [>] there must be [<] and if [<] there must be [>]
+        if bd.descriptor in ("<", ">"):
+            bd_pair = _find_complementing_bonding_descriptor(stoch_obj, bd)
+            if bd_pair is None:
+                raise ValidationError(f'{bd} complementary partner not found.')
+
+
+def _find_complementing_bonding_descriptor(stoch_obj: StochasticObject, bond_descr: BondDescriptor) \
+        -> BondDescriptor | None:
+    for bd in stoch_obj.bonding_descriptors:
+        if bond_descr.is_pair(bd):
+            return bd
+
+    return None
+
+
+def check_implicit_endgroups_ends(obj: has_node_attr, parent_obj: has_node_attr = None):
+    # if end is implicit; there must be single bonding units
+    for i, node in enumerate(obj.nodes):
+        if isinstance(node, StochasticObject):
+            if node.bd_left.implicit:
+                if i != 0:
+                    # nothing allowed to the left
+                    raise ValidationError("With a the left end group implicit, "
+                                          "there should be nothing to the left of the stochastic object.")
+                if parent_obj is not None:
+                    # if it isn't BigSMILES it can't have a left implicit end group
+                    raise ValidationError("Implicit left end group not allowed within interior.")
+
+            if node.bd_right.implicit:
+
+                if i != len(obj.nodes) - 1:
+                    # nothing allowed to the right
+                    raise ValidationError("With a implicit right end group, there should not be anything to the "
+                                          "right of it.")
+
+        if hasattr(node, "nodes"):
+            check_implicit_endgroups_ends(node, obj)
diff --git a/src/bigsmiles/validation/validation_string.py b/src/bigsmiles/validation/validation_string.py
new file mode 100644
index 0000000..681ee8f
--- /dev/null
+++ b/src/bigsmiles/validation/validation_string.py
@@ -0,0 +1,142 @@
+"""
+
+Validation on SMILES or BigSMILES string.
+
+"""
+from __future__ import annotations
+import re
+
+from bigsmiles.errors import ValidationError
+
+
+def run_string_validation(text: str) -> str:
+    """ Main entry point for pre-construction validation. """
+    branch_symbol_validation(text)
+    stochastic_object_validation(text)
+    brackets_validation(text)
+    # text = validate_ring_numbering(text)  # Faster to do after tokenization
+    return text
+
+
+def branch_symbol_validation(text: str):
+    """ Confirm that there is an equal number of '(' to ')'. """
+    count = text.count('(') - text.count(')')
+    if count != 0:
+        if count > 0:
+            raise ValidationError(f"Invalid BigSMILES. Missing {count} closing branch symbols ')'. "
+                                  f"\n Invalid string: {text}")
+        raise ValidationError(f"Invalid BigSMILES. Missing {count} opening branch symbols '('. "
+                              f"\n Invalid string: {text}")
+
+
+def stochastic_object_validation(text: str):
+    """ Confirm that there is an equal number of '{' to '}'. """
+    count = text.count('{') - text.count('}')
+    if count != 0:
+        if count > 0:
+            raise ValidationError(f"Invalid BigSMILES. Missing {count} closing stochastic object symbols"
+                                  + " '}'. " + f"\n Invalid string: {text}")
+        raise ValidationError(f"Invalid BigSMILES. Missing {count} opening stochastic object symbols "
+                              + "'{'. " + f"\n Invalid string: {text}")
+
+
+def brackets_validation(text: str):
+    """ Confirm that there is an equal number of '[' to ']'. """
+    count = text.count('[') - text.count(']')
+    if count != 0:
+        if count > 0:
+            raise ValidationError(f"Invalid BigSMILES. Missing {count} closing bracket symbols ']'. "
+                                  f"\n Invalid string: {text}")
+        raise ValidationError(f"Invalid BigSMILES. Missing {count} opening bracket symbols '['. "
+                              f"\n Invalid string: {text}")
+
+
+ring_digit_pattern = re.compile(r"(?<![H+><$-]|\[)(%\d{2}|\d)")
+extract_bracket_pattern = re.compile(r"(\[)(.*?)(\])")
+
+ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
+len_ascii_lowercase = len(ascii_lowercase)
+
+
+def get_next_letter(num: int) -> str:
+    num_sym = int(num/len_ascii_lowercase)
+    mod_sym = num % len_ascii_lowercase
+
+    if num == 0:
+        sym = ascii_lowercase[mod_sym]
+    else:
+        sym = ascii_lowercase[mod_sym] * (num_sym + 1)
+
+    return "<" + sym + ">"
+
+
+def validate_ring_numbering(text: str, renumber: bool = True):
+    """
+    Confirm that there is only two of a number.
+
+    Note: Faster to do after tokenization
+    """
+    # No rings
+    if text.count("1") == 0:
+        return text
+
+    # remove atoms with special stuff as it mess with ring parsing
+    extract = dict()
+    split_list = re.split(extract_bracket_pattern, text)
+    for i in range(len(split_list)):
+        if split_list[i] == "[":
+            replacement_symbol = get_next_letter(i)
+            extract[replacement_symbol] = split_list[i + 1]
+            split_list[i + 1] = replacement_symbol
+
+    text = "".join(split_list)
+
+    ring_numbers = [int(num.replace("%", "")) for num in ring_digit_pattern.findall(text)]
+    ring_number_set = set(ring_numbers)
+
+    # check for more than two
+    for num in list(ring_number_set):
+        count = ring_numbers.count(num)
+        if count != 2:
+            if count == 1:
+                raise ValidationError(f"Invalid BigSMILES. Missing ring index. Only one found for: '{num}'. "
+                                      f"\n Invalid string: {text}")
+            elif renumber and count % 2 == 0:
+                # renumber duplicates
+                text = _renumber_rings(text, num, max(ring_number_set) + 1)
+                for i in range(1, int(count / 2) + 1):
+                    ring_number_set.add(max(ring_number_set) + i)
+
+            else:
+                raise ValidationError(f"Invalid BigSMILES. More than 2 ring index found for: '{num}'. "
+                                      f"\n Invalid string: {text}")
+
+    # do replacement
+    for k, v in extract.items():
+        text = text.replace(k, v)
+    return text
+
+
+def _renumber_rings(text: str, num: int, new_num: int) -> str:
+    if num > 9:
+        pattern = r"(?<![H+><$-]|\[)" + f"(%{num})"
+    else:
+        pattern = r"(?<![H+><$-%]|\[)" + f"({num})"
+
+    hit_index = [(m.start(0), m.end(0)) for m in re.finditer(pattern, text)]
+
+    new_num += int(len(hit_index) / 2 - 2)
+    first = True
+    for hit in reversed(hit_index[2:]):  # skip the first two
+        if new_num > 9:
+            text = text[:hit[0]] + "%" + str(new_num) + text[hit[1]:]
+        else:
+            text = text[:hit[0]] + str(new_num) + text[hit[1]:]
+
+        if first:
+            first = False
+        else:
+            first = True
+            new_num -= 1
+
+    return text
diff --git a/src/bigsmiles/validation/validation_tokens.py b/src/bigsmiles/validation/validation_tokens.py
new file mode 100644
index 0000000..2254b41
--- /dev/null
+++ b/src/bigsmiles/validation/validation_tokens.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+import logging
+
+from bigsmiles.errors import ValidationError
+from bigsmiles.constructors.tokenizer import Token, TokenKind
+
+
+def run_token_validation(tokens: list[Token]) -> list[Token]:
+    tokens = validate_ring_numbering(tokens)
+
+    return tokens
+
+
+def validate_ring_numbering(tokens: list[Token], renumber: bool = True) -> list[Token]:
+    """ Confirm that there is only two of a number. """
+
+    # get ring_numbers
+    ring_numbers = dict()  # key: ring number; value: list[index in tokens]
+    for i, token in enumerate(tokens):
+        if token.kind is TokenKind.Ring:
+            value = int(token.value)
+        elif token.kind is TokenKind.Ring2:
+            value = int(token.value.replace("%", ""))
+        else:
+            continue
+
+        if value in ring_numbers:
+            ring_numbers[value].append(i)
+        else:
+            ring_numbers[value] = [i]
+
+    # No rings
+    if not ring_numbers:
+        return tokens
+
+    # check for more than two
+    max_index = max([k for k in ring_numbers.keys()]) + 1
+    for key in list(ring_numbers.keys()):
+        count = len(ring_numbers[key])
+        if count == 2:
+            continue
+        elif count == 1:
+            raise ValidationError(f"Invalid BigSMILES. Missing ring index. Only one found for: '{key}'. ")
+        elif renumber and count % 2 == 0:
+            logging.warning(f"Duplicate ring index detected ({key}) and fixed for: "
+                            f"{''.join([str(t.value) for t in tokens])}")
+            # renumber duplicates
+            for i, index_ in enumerate(ring_numbers[key]):
+                if int(i/2) == 0:
+                    continue  # don't change the first set
+
+                tokens[index_].value = str(max_index) if max_index < 10 else "%" + str(max_index)
+                if i % 2 != 0:
+                    max_index += 1
+        else:
+            raise ValidationError(f"Invalid BigSMILES. More than 2 ring index found for: '{key}'. ")
+
+    return tokens


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..282622a
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,34 @@
+name: Tests
+
+on:
+  - pull_request
+
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.7", "3.10", "3.11"]
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements_dev.txt
+
+      - name: Install CRIPT Python SDK
+        run: pip install .
+
+      - name: Test with pytest
+        run: |
+          pytest --cov --cov-report=html
+          
diff --git a/tests/README.md b/tests/README.md
index 78ed6ba..4a5e456 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -6,6 +6,7 @@ Test function of code against known answers.
 
 Run `main.py`.
 
+Run with coverage report `pytest --cov --cov-report=html:coverage_re`
 
 
 ## performance
diff --git a/tests/bigsmiles/test_bigsmiles.py b/tests/bigsmiles/test_bigsmiles.py
index 2972a0e..ebb3b50 100644
--- a/tests/bigsmiles/test_bigsmiles.py
+++ b/tests/bigsmiles/test_bigsmiles.py
@@ -1,30 +1,116 @@
+import logging
+
 import pytest
 
 import bigsmiles
+from bigsmiles.errors import BigSMILESError
 
+bigsmiles.Config.show_aromatic_bond = False
 
 test_molecules = [
-
-    # daylight tests
+    # simple
+    "C",
+    "Cl",
+    "[U]",
+    "[Zn]",
+    "Cn",
+    "[Cn]",
+    "CC",
     "CCCCCC",  # hexane
     "O=C=O",  # carbon dioxide
+    "CC#CC",  # 2-butyne
     "C#N",  # hydrogen cyanide
     "CCN(CC)CC",  # triethylamine
     "CC(=O)O",  # acetic acid
     "C1CCCCC1",  # cyclohexane
+
+    # rings
+    "C=1CCCCC1",
+    "C12C3C4C1C5C4C3C25",
+    "O1CCCCC1N2CCCCC2",
+    "C12(CCCCC1)CCCCC2",  # spiro[5.5]undecane
+
+    # aromatic_elements
     "c1ccccc1",  # benzene
-    # "C1=CN=C[NH]C(=O)1",  # ring number
-    # "c1cnc[nH]c(=O)1",  # ring number
+    "n1ccccc1",  # pyridine
+    "Cn1cccc1",  # Methyl-pyrrole
+    "[nH]1cccc1",  # 1H-pyrrole
+    "O=c1[nH]cccc1",  # 2-pyridone
+    "Oc1ncccc1",  # 2-pyridinol
+    "C1=CC=CC(CCC2)=C12",  # indane
+    "c1ccc2CCCc2c1",  # indane
+    "C1=CC=PC=C1",  # Phosphorine
+    "c1ccp(=O)cc1",  # Phosphorine oxide
+    "O=P1=CC=CC=C1",  # Phosphorine oxide
+    "O=p1ccccc1",  # Phosphorine oxide
+    "c1ccccc1-c2ccccc2",
+    "C1(C=C2)=C3C2=CC=C3C=C1",  # Acepentalene
+
+    # cis/trans
+    "F/C=C/F",  # E-difluoroethene
+    r"F\C=C\F",  # Z-difluoroethene
+    r"C(\F)=C/F",  # trans
+    r"F\C=C/F",  # cis
+    r"F/C=C\F",  # cis
+    "C(/F)=C/F",  # cis
+    "F/C(CC)=C/F",
+    "F/C=C=C=C/F",  # trans
+    r"F/C=C=C=C\F",  # cis
+    r"F/C=C/C/C=C\C",
+    "F/C=C/CC=CC",  # partially specified
+
+    # charges
+    "[Fe+]",
+    "[Fe+2]",
+    "[OH-]",
+    "[OH3+]",
+    "[O-][n+]1ccccc1",  # Pyridine-N-oxide
+    "[O-][N+]1=CC=CC=C1",
+
+    # chiral
     "N[C@@H](C)C(=O)O",  # L-alanine
     "N[C@H](C)C(=O)O",  # D-alanine
     "N[C@](C)(F)C(=O)O",
     "N[C@@](F)(C)C(=O)O",
     "C[C@H]1CCCCO1",
     "O1CCCC[C@@H]1C",
-    # "F/C=C/F",  # E-difluoroethene
-    # "F\C=C\F",  # Z-difluoroethene
+    "N[P@](C(O)=O)C",
+
+    # disconnected structures
+    "C1.CCCCC1",  # hexane
+    "[Na+].[Cl-]",  # sodium chloride
+    "[Na+].[O-]c1ccccc1",  # sodium phenoxide
+    "c1cc([Na+].[O-])ccc1",  # sodium phenoxide
+    "[NH4+].[NH4+].[O-]S(=O)(=O)[S-]",  # diammonium thiosulfate
+    "c1c2c3c4cc1.Br2.Cl3.Cl4",  # 1-bromo-2,3-dichlorobenzene
+
+    # atom indexing
+    "[CH3:1][CH2:2][CH2:3][CH2:4][CH2:5][CH3:6]",  # hexane
+    "[CH2:1]1[CH2:2][CH:3]=[CH:4][CH2:5][CH2:6]1",  # cylcohexene
+    "[CH3:1]CC[CH2:2][C:3]",  # hexane
+    "[CH2:1]=[CH:2][CH2:1][CH2:3][CH:4](C)[CH3:3]",
+    "[OH:1][C:2](=[O:3])[CH:4]([CH3:5])[OH:6]",  # lactic acid
+
+    # metal complex
+    "[Rh-](Cl)(Cl)(Cl)(Cl)$[Rh-](Cl)(Cl)(Cl)Cl",
 
+    # extended atom
+    "[C]",
+    "[Fe]",
+    "[12C]",
+    "[238U]",
+    "[13CH4]",
+    "[12C@@H2+]",
+    "[38Br-]",
+    "[34SH2+2]",
+    "[CH4]",
+    "[H][CH2][H]",
+    "[H][CH]([H])[H]",
+    "[ClH]",
+    "[2H][CH2]C",
 
+    # complex / random
+    "OS(=O)(=S)O",
     "CC1=C(C(=NC=C1C=O)C)O",
     "C1=CC(C(C(=C1)C(=O)O)O)O",
     "C(C(=O)COP(=O)(O)O)N",
@@ -37,10 +123,6 @@
     "CC(CC1=CC(=C(C=C1)O)O)(C(=O)OC)N",
     "C1=CC=C(C=C1)S(=O)(=O)NNC2=NC(=NC(=N2)Cl)Cl",
     "C=CC(CCC)C(C(C)C)CCC",  # 3-propyl-4-isopropyl-1-heptene
-    "O1CCCCC1N2CCCCC2",
-
-    "C12C3C4C1C5C4C3C25",
-
     "[12C]C1=C(C(=NC=C1C=O)C)O",
     "[C@]C1=C(C(=NC=C1C=O)C)O",
     "[35Br]CC1=C(C(=NC=C1C=O)C)O",
@@ -50,23 +132,12 @@
     "[Fe+3]CCC",
     "CC[12C]C1=C(C(=NC=C1C=O)C)O",
     "CC[C@]C1=C(C(=NC=C1C=O)C)O",
-    "CC[35Br]CC1=C(C(=NC=C1C=O)C)O",
-    "CC[35Br@@]CC1=C(C(=NC=C1C=O)C)O",
     "CC[12CH2]C1=C(C(=NC=C1C=O)C)O",
     "CC[BrH3]",
     "CC[Fe+3]CCC",
 
-    # cis/trans
-
-    # "C(\F)=C/F",  # trans
-    # "F\C=C/F",  # cis
-    # "F/C=C\F",  # cis
-    # "C(/F)=C/F",  # cis
-    # "F/C(CC)=C/F",
-    # "F/C=C=C=C/F",  # trans
-    # "F/C=C=C=C\F", # cis
-    # "F/C=C/C/C=C\C",
-    # "F/C=C/CC=CC",  # partially specified
+    "FC=[C@@]=CF",  # allenes are axial chiral (all even number of double bonds)
+    # Cumulene and all odd number of double bonds E/Z
 
 ]
 
@@ -78,24 +149,72 @@ def test_whole_system_molecule(molecule: str):
 
 
 test_polymers = [
-    "[H]O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}[H]",
-    "{[>][$]CC[$],[$]CC(CC)[$][<]}",
-    "{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}",
-    "{[>][<]C(=O)CCCCC(=O)NCCCCCCN[>][<]}",
-    "C{[$][$]CC[$],[$]CC(CC)[$][$]}",
+    # homopolymer
+    "[H]{[$][$]CC[$][$]}[H]",  # poly(ethylene)
+    "[H]{[>][<]CC[>][<]}[H]",
+    "[H]{[$][$]CC(C)[$][$]}[H]",  # poly(propylene) random orientation
+    "[H]{[>][<]CC(C)[>][<]}[H]",
+    "[H]{[>][<]CC(C1=CC=CC=C1)[>][<]}[H]",  # PS head-to-tail
+    "[H]{[>][<]CC(C1)=CC=CC=C1[>][<]}[H]",
+    "[H]{[>][<]NCCCCCC(=O)[>][<]}O",  # nylon 6
+    "[H]{[>][<]OC(C)C(=O)[>][<]}O",  # PLA
+    "O{[>][<]C(=O)C(C)O[>][<]}[H]",
+    "[H]{[>][<]CCO[>][<]}[H]",  # polyethylene oxide
+    "[H]{[>][<]CC(C)O[>][<]}[H]",  # polypropylene oxide
+    "[H]{[>][<]CC(C#N)[>][<]}[H]",  # polyacrylonitrile
+    "[H]{[>][<]CC=CC[>][<]}[H]",  # polybutadiene
+    r"[H]{[<][>]C\C=C\C[<][>]}[H]",  # polybutadiene (trans)
+    r"[H]{[<][>]C\C=C/C[<][>]}[H]",  # polybutadiene (cis)
+    # with branches
+    "[H]{[<][$]CC(C)(CC)[$][>]}[H]",
+    "[H]{[<][$]CC(CCC)[$][>]}[H]",
+    "[H]{[<][$]CC(C(CC)C)[$][>]}[H]",
+    "CC{[>][<]CC(C)[>][<]}CC(C)=C",
+    # with rings
+    "[H]{[<][$]C1CC(C1)[$][>]}[H]",  # 4 membered rings
+    "[H]{[<][$]C1CCC(C1)[$][>]}[H]",  # 5 membered rings (3, 2)
+    "[H]{[<][$]C1CC(CC1)[$][>]}[H]",  # 5 membered rings
+    "[H]{[<][$]C1(CCCCC1)[$][>]}[H]",  # 6 membered rings
+    # multiple bonds
+    "C={[$][$]=CC=[$][$]}=C",  # polyacetylene (traditional)
+    "C={[$][$]=C[$2][$2]}[H]",  # polyacetylene (minimal)
+    "C#{[$][$]#CC1=CC=C(C=C1)C#[$][$]}#C",
+
+    # copolymers
     "C{[$][$]CC[$],[$]CC(CC[$])[$][$]}O",
-    "CC{[>][<]CC(C)[>][<]}CC(C)=C",  # explicit end groups
-    "{[][$]CC[$],[$]CC(CC)[$][]}",  # implicit end groups
-    "{[]C([$])C([$])CC[]}",  # test end groups in middle
+
+    # implicit end groups
+    "{[][$]CC[$],[$]CC(CC)[$][]}",
+    "{[][$]=CC=[$][]}",
+
+    # mixed end groups
+    "{[][>]C([>])[>],[<]C[>][>]}C",
+    "[H]{[<][>]OCCO[>],[<]C(=O)C1=CC=C(C=C1)C(=O)[<],[>]O[]}",  # PET
+    "{[][>]NCCCCCCN[>],[<]C(=O)CCCCC(=O)[<],[>]Cl,[<][H][]}",  # nylon 6,6
+
+    # test end groups in middle
+    "{[]C([$])C([$])CC[]}",
+    "[H]{[$]C([$])C([$])CC[$]}[H]",
+
+    # nested linear
     "OC{[>][<]CC(C{[>][<]CCO[>][<]}CN)[>][<]}CC",
+
+    # bonding descriptor index
     "CC(CC){[<][>]CC(C)[<2][>2]}CCO",
-    # "{[][>]C([>])([>]),[<]OO[>][>]}CB", Not sure if it a valid BigSMILES
 
-    # From BCPD
+    # alternating
+    "[H]O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}[H]",
+    "[H]{[>][<]CC(C1=CC=CC=C1)[<],[>]C2C(C(=O)OC2=O)[>][<]}[H]",  # PS-alt-poly(maleic anhydride)
+
+    # block copolymer
+    "[H]{[$][$]CC[$][$]}{[$][$]CC[$][$]}[H]",
+    '[H]{[$][$]CC[$][$]}{[$][$]CC(C)[$][$]}[H]',  # poly(ethylene)-block-poly(propylene)
     "CCC(C){[$][$]CC(C1CCCCC1)[$][$]}{[$][$]CCCC[$],[$]CC(CC)[$][$]}[H]",
-    # "{[][<]CCO[>][<]}{[$][$]C\C=C(C)/C[$],[$]C\C=C(C)\C[$],[$]CC(C(C)=C)[$],[$]CC(C)(C=C)[$][]}",
-    # "{[][$]C\C=C/C[$],[$]C\C=C\C[$],[$]CC(C=C)[$][$]}{[>][<][Si](C)(C)O[>][]}",
-    # "{[][$]C\C=C(C)/C[$],[$]C\C=C(C)\C[$],[$]CC(C(C)=C)[$],[$]CC(C)(C=C)[$][$]}{[$][$]CC(c1ccccc1)[$][]}",
+    '[H]{[>][<]OCC[>][<]}O{[>][<]C(=O)C(C)O[>][<]}[H]',  # PEG-Oxygen-PLA
+    'O{[>][<]CCO[>][<]}{[>][<]C(=O)C(C)O[>][<]}[H]',  # Oxygen - PEG-block-PLA
+    r"{[][<]CCO[>][<]}{[$][$]C\C=C(C)/C[$],[$]C\C=C(C)\C[$],[$]CC(C(C)=C)[$],[$]CC(C)(C=C)[$][]}",
+    r"{[][$]C\C=C/C[$],[$]C\C=C\C[$],[$]CC(C=C)[$][$]}{[>][<][Si](C)(C)O[>][]}",
+    r"{[][$]C\C=C(C)/C[$],[$]C\C=C(C)\C[$],[$]CC(C(C)=C)[$],[$]CC(C)(C=C)[$][$]}{[$][$]CC(c1ccccc1)[$][]}",
     "{[][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CCCC[$],[$]CC(CC)[$][]}",
     "COCCOCCO{[>][<]CCO[>][<]}{[>][<]C(CC)CO[>][<]}[H]",
     "{[][<]CCO[>][<]}{[>][<]C(CC)CO[>][]}",
@@ -105,13 +224,54 @@ def test_whole_system_molecule(molecule: str):
     # "{[][$]CC(c1ccccc1)[$][$]}{[$][$]C\C=C(C)/C[$],[$]C\C=C(C)\C[$],[$]CC(C(C)=C)[$],[$]CC(C)(C=C)[$][]}",
     # "{[][$]C\C=C(C)/C[$],[$]C\C=C(C)\C[$],[$]CC(C(C)=C)[$],[$]CC(C)(C=C)[$][$]}{[$][$]CC(c1ccccc1)[$][]}",
     # "[H]{[>][>]CCO[<][<]}{[$][$]C\C=C(C)/C[$],[$]C\C=C(C)\C[$],[$]CC(C(C)=C)[$],[$]CC(C)(C=C)[$][$]}C(C)CC",
-    "{[][$]CC(c1ccccc1)[$],[$]CC(c1ccc(S(=O)(=O)O)cc1)[$][$]}{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][]}",
     "{[][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CCCC[$],[$]CC(CC)[$][]}",
-    "{[][$]CC(C1CCCCC1)[$][$]}{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][$]}"
-    "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][$]}"
-    "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][$]}"
-    "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][$]}"
-    "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][]}",
+    "{[][$]CC(C1CCCCC1)[$][$]}{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C2CCCCC2)[$][$]}"
+    "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C3CCCCC3)[$][$]}"
+    "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C4CCCCC4)[$][$]}"
+    "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C5CCCCC5)[$][$]}"
+    "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C6CCCCC6)[$][]}",
+    # multi bonds
+    "C={[$][$]=CC=[$][$]}={[$][$]=CC=[$][$]}=C",
+    "C={[$][$]=CCCCCCCC=[$],[$]=CC1CCC(C1)C=[$][$]}=C",  # poly(cyclooctene)-rand-poly(noroborene)
+
+    # star
+    'CC({[$][$]CC[$][$]}[H])CCC',
+    'CC({[$][$]CC[$][$]}[H])({[$][$]CC[$][$]}[H])CCC',
+    'CC({[$][$]CC[$][$]}[H])({[$][$]CC[$][$]}O)CCC',
+    'CC({[$][$]CC[$][$]}N)({[$][$]CC[$][$]}O)CCC',
+
+    # # graft/bottlebrush
+    "[H]{[>][<]CC([>2])[>],[<2]CC[>2],[<2][H][<]}[H]",  # bottlebrush
+    "[H]{[>][<]CC([>2])[>],[<]CC(C)[>],[<2]CC[>2],[<2][H][<]}[H]",  # graft copolymerization
+    "[H]{[>][<]CC([>2])[>],[<2]CC[>2],[<2][>3],[<3]CC(C)[>3],[<3][H][<]}[H]",  # graft diblock brushes
+    "C={[>][<]=CC([>2])=[>],[<2]CC[>2],[<2][>3],[<3]CC(C)[>3],[<3][H][<]}=C",  # graft diblock brushes (double bond)
+    "C={[>][<]=CC(=[>2])=[>],[<2]=CC=[>2],[<2]=[>3],[<3]=CC(C)=[>3],[<3]=C[<]}=C",  # graft diblock brushes(double bond)
+    "[H]{[>][<]CC([>2])[>],[<2]CC(C)[>2],[<2]CC[>2],[<2][H][<]}[H]",  # graft random copolymer brushes
+    # nesting
+    "[H]{[>][<]CC({[>][<]CC[>][<]}[H])[>][<]}[H]",  # bottlebrush
+
+    # hyperbranched
+    "[H]{[$][$]CC([$])[$][$]}[H]",  # hyperbranch Poly(ethylene)
+    "[H]{[$][$]CC([$])[$],[$]CC[$][$]}[H]",  # hyperbranch Poly(ethylene)
+
+    # rings/polycyclic (stochastic)
+    "C1{[$][$]CC[$][$]}1",
+    "C({[>][<]CCO[>][<]}1)CCCCCC1",
+    "C1{[>][<]CCO[>][<]}CO1",  # PEG rings
+    "C({[>][<]CCO[>][<]}1)CCO1",  # PEG rings wierd written
+    "C({[>][<]CCO[>][<]}1)({[>][<]CCO[>][<]}2)OCC12",  # dual PEG rings
+    "C({[>][<]CCO[>][<]}1)({[>][<]CCO[>][<]}2)({[>][<]CCO[>][<]}3)OCC123",  # triple PEG rings
+    # multiple bonds
+    "C(={[>][<]=CC=[>][<]}=1)CCCCCC=1",
+    "C=1CCC(={[>][<]=CC=[>][<]}=1)CCC",
+    # aromatic_elements notation
+    "c1c{[$][$]cc[$][$]}1",  # cyclic polyacetylene
+
+    # networks
+    "{[][$]CC=CC[$],[$]CC([<])C([>])C[$],[>]S[<],[$]C[]}",  # poly(1,4-butadiene) rubber vulcanized
+    "[H]{[>][<]CCN([>])[<][<]}[H]",  # branched polyethylenimine
+
+    # ladder
 
 ]
 
@@ -122,26 +282,145 @@ def test_whole_system(polymer: str):
     assert str(result) == polymer
 
 
+cases_with_changes = [
+    # hydrogens
+    ["[ClH1]", "[ClH]"],
+
+    # charges
+    ["[OH-1]", "[OH-]"],
+    ["[Fe++]", "[Fe+2]"],
+    ["[12C@@H2+1]", "[12C@@H2+]"],
+    ["[O-1][n+1]1ccccc1", "[O-][n+]1ccccc1"],
+
+    # branching
+    ["C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C))))))))))))))))))))C",
+     "C(CCCCCCCCCCCCCCCCCCCC)C"],
+
+    # rings
+    ["C=1CCCCC=1", "C=1CCCCC1"],
+    ["C1CCCCC=1", "C=1CCCCC1"],
+    ["C1=CN=C[NH]C(=O)1", "C1=CN=C[NH]C1=O"],  # ring number position change
+    ["c1cnc[nH]c(=O)1", "c1cnc[nH]c1=O"],  # ring number position change
+    ["C(={[>][<]=CC=[>][<]}12)CCCCCC12", "C(={[>][<]=CC=[>][<]}=1)CCCCCC=1"],
+    ["C2(C=C3)=C1C3=CC=C1C=C2", 'C1(C=C2)=C3C2=CC=C3C=C1'],  # Acepentalene
+
+    # polymers
+    ["{[][>]C([>])([>]),[<]C[>][>]}C", "{[][>]C([>])[>],[<]C[>][>]}C"],  # drop () around bond descriptor
+    ["[H]{[>][<]CC(C1=CC=CC=C1)[<],[>]C2C(C(=O)OC2(=O))[>][<]}[H]",
+     "[H]{[>][<]CC(C1=CC=CC=C1)[<],[>]C2C(C(=O)OC2=O)[>][<]}[H]"],  # drop () around =O
+    ["[H]{[>1][<]CC([>2])[>],[<2]CC[>2],[<2][H][<1]}[H]", "[H]{[>][<]CC([>2])[>],[<2]CC[>2],[<2][H][<]}[H]"],  # drop 1
+    ["C(={[>][<]=CC=[>][<]}=1)CCCCCC1", "C(={[>][<]=CC=[>][<]}=1)CCCCCC=1"],  # add double bond to second ring
+    ["C(={[>][<]=CC=[>][<]}1)CCCCCC=1", "C(={[>][<]=CC=[>][<]}=1)CCCCCC=1"],  # add double bond to second ring
+    ["C1CCC(={[>][<]=CC=[>][<]}=1)CCC", "C=1CCC(={[>][<]=CC=[>][<]}=1)CCC"],  # add double bond to second ring
+    ["C=1CCC(={[>][<]=CC=[>][<]}1)CCC", "C=1CCC(={[>][<]=CC=[>][<]}=1)CCC"],  # add double bond to second ring
+
+    # random
+    ["C1.C1", "CC"],  # disconnect and ring notation cancel out
+]
+
+
+@pytest.mark.parametrize("case", cases_with_changes)
+def test_whole_system_with_changes(case: list):
+    input_, answer = case
+    result = bigsmiles.BigSMILES(input_)
+    assert str(result) == answer
+
+
+ring_fix_cases = [
+    ["O1CCCCC1N1CCCCC1", "O1CCCCC1N2CCCCC2"],  # re-use of ring index],  # re-use of ring index
+    ["[H]{[>][<]CC(C1=CC=CC=C1)[<],[>]C1C(C(=O)OC1=O)[>][<]}[H]",
+     "[H]{[>][<]CC(C1=CC=CC=C1)[<],[>]C2C(C(=O)OC2=O)[>][<]}[H]"],
+    ["{[][$]CC(c1ccccc1)[$],[$]CC(c1ccc(S(=O)(=O)O)cc1)[$][$]}{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][]}",
+     "{[][$]CC(c1ccccc1)[$],[$]CC(c2ccc(S(=O)(=O)O)cc2)[$][$]}{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][]}"],
+    ["{[][$]CC(C1CCCCC1)[$][$]}{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C2CCCCC2)[$][$]}"
+     "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][$]}"
+     "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][$]}"
+     "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][$]}"
+     "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C1CCCCC1)[$][]}",
+     "{[][$]CC(C1CCCCC1)[$][$]}{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C2CCCCC2)[$][$]}"
+     "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C3CCCCC3)[$][$]}"
+     "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C4CCCCC4)[$][$]}"
+     "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C5CCCCC5)[$][$]}"
+     "{[$][$]CCC(C)C[$],[$]CC(C(C)C)[$],[$]CC(C)(CC)[$][$]}{[$][$]CC(C6CCCCC6)[$][]}"
+     ]
+
+]
+
+
+@pytest.mark.parametrize("case", ring_fix_cases)
+def test_ring_fix(caplog, case: list):
+    input_, answer = case
+    with caplog.at_level(logging.WARNING):
+        result = bigsmiles.BigSMILES(input_)
+    assert str(result) == answer
+    assert "Duplicate ring index detected" in caplog.text
+
+
+cases_add_explicit_hydrogens = [
+    ["{[$][$]CC[$][$]}", "[H]{[$][$]CC[$][$]}[H]"],
+    ["O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}", "O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}[H]"],
+    ["{[$][$]CC[$],[$]CC(CC)[$][$]}", "[H]{[$][$]CC[$],[$]CC(CC)[$][$]}[H]"],
+    ["{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}", "[H]{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}[H]"],
+    ["{[>][<]C(=O)CCCCC(=O)NCCCCCCN[>][<]}", "[H]{[>][<]C(=O)CCCCC(=O)NCCCCCCN[>][<]}[H]"],
+    ["C{[$][$]CC[$],[$]CC(CC)[$][$]}", "C{[$][$]CC[$],[$]CC(CC)[$][$]}[H]"],
+]
+
+
+@pytest.mark.parametrize("case", cases_add_explicit_hydrogens)
+def test_add_explicit_hydrogens(case: list):
+    input_, answer = case
+    result = bigsmiles.BigSMILES(input_)
+    assert str(result) == answer
+
+
+cases_incomplete_valance = [
+    ["N1=NN=C[N]1", "N1=NN=C[N]1"],  # under-saturated nitrogen
+    ["CCC[12C]CCC", "CCC[12C]CCC"],  # under-saturated carbon
+    ["[CH2]CCCC", "[CH2]CCCC"],  # under-saturated carbon
+    ["[CH]C=C", "[CH]C=C"],  # under-saturated carbon
+    ["[CH2]C=C", "[CH2]C=C"],  # under-saturated carbon
+    ["CC[35Br]CC1=C(C(=NC=C1C=O)C)O", "CC[35Br]CC1=C(C(=NC=C1C=O)C)O"],  # over-saturated Br
+    ["CC[35Br@@]CC1=C(C(=NC=C1C=O)C)O", "CC[35Br@@]CC1=C(C(=NC=C1C=O)C)O"],  # over-saturated Br
+    ["C(C)(C)(C)(C)C", "C(C)(C)(C)(C)C"],  # over-saturated C
+    ["O=n1ccccc1", "O=n1ccccc1"],  # Pyridine-N-oxide
+]
+
+
+@pytest.mark.parametrize("case", cases_incomplete_valance)
+def test_incomplete_valance(caplog, case: list):
+    input_, answer = case
+    with caplog.at_level(logging.WARNING):
+        result = bigsmiles.BigSMILES(input_)
+    assert str(result) == answer
+    assert "Incomplete valence detected" in caplog.text
+
+
 validation_cases = [
+    ["DJW"],  # not an symbol
+    ["[C"],
+    ["[C]]"],
+    ["[]"],
+    ["[.]"],
+    ["[rings=]"],
     ["CCCCC1"],  # Ring not closed
     ["CCCC("],  # branch not closed
     ["CCCC)"],  # branch not started
-    ["((CC))"],   # no double branch/ extra parenthesis
-    ["C((C)C)"],   # no branch right away
-    ["C(C)(C)(C)(C)C"],  # break bond limit of carbon
-    ["O1CCCCC1N1CCCCC1"],  # re-use of ring index
+    ["((CC))"],  # no double branch/ extra parenthesis
+    ["C((C)C)"],  # no branch right away
+
     # ['C/C(\F)=C/C'],  # conflicting cis/trans
 
     # bigsmiles
     ["CCC,C"],  # stochastic seperator
     ["CC}CC"],  # stochastic object no start
-    ["CC{CC"],   # stochastic object no end
+    ["CC{CC"],  # stochastic object no end
     ["{CC}"],
     ["{[]CC[]}"],
     # ["{[][$]CC[]}"],  # are these valid?
-    # ["CC({[][$]CC[]})CC"],
+    ["CC({[][$]CC[]})CC"],
     # ["CC({[$][$]CC[$]})CC"],
     # ["CC(C{[$][$]CC[$]})CC"],
+    # ["{[>][$]CC[$],[$]CC(CC)[$][<]}"],
     ["{[][>]CC[$][]}"],
     ["{[][>]CC[>][]}"],
     ["{[][>]CC[>][]}CC"],  # implicit and explict end groups same time
@@ -150,22 +429,56 @@ def test_whole_system(polymer: str):
     # ["{[][>]CC[>];[$]C[]}"],
     # ["{[$2][$]CC[$][$2]}"],  # index don't match
     ["{[$][<]CC[>][$]"],  # end group bonding descriptor don't match stochastic fragment
+    ["C={[$][$]=C[$][$]}[H]"],  # multiple bond types to [$]
+    ["C{[>][<]=CC([>2])=[>],[<2]CC[>2],[<2][>3],[<3]CC(C)[>3],[<3][H][<]}C"],  # multiple bond types to [>]
+    ["{[$][$]=CC=[$][$]}"],  # end group must have double bond
+    ["C={[$][$]=CC=[$][$]}"],  # end group must have double bond
+    ["{[$][$]=CC=[$][$]}=C"],  # end group must have double bond
+    ["1{[$][$]CC[$][$]}1"],  # ring id can't start
+    ["={[$][$]CC[$][$]}1"],  # ring id can't start
+    ["[$]{[$][$]CC[$][$]}1"],  # ring id can't start
+    ["C1{[$][$]CC[$][$]}1C"],  # last carbon has nothing to bond to
+    ["C{[>][<]=CC=([>2])=[>],[<2]CC[>2],[<2][>3],[<3]CC(C)[>3],[<3][H][<]}C"],  # first repeat extra =
+    ["C{[>][<]=CC=([>2])[>],[<2]CC[>2],[<2][>3],[<3]CC(C)[>3],[<3][H][<]}C"],  # first repeat wrong = location
+
+    # ["C{[$][$]C.C[$][$]}C"]
+
 ]
 
 
 @pytest.mark.parametrize("polymer", validation_cases)
 def test_validation(polymer: list):
-    with pytest.raises(bigsmiles.BigSMILESError) as e:
+    with pytest.raises(BigSMILESError) as _:
         bigsmiles.BigSMILES(polymer[0])
 
+# case_for_conjugated = [
+#     ["c1ccccc1", {0: True, 1: True, 2: True, 3: True, 4: True, 5: True}],  # benzene
+#     ["c1cnccc1", {0: True, 1: True, 2: True, 3: True, 4: True, 5: True}],  # pyridine
+#     ["c1cocc1", {0: True, 1: True, 2: True, 3: True, 4: True, 5: True}],  # furan
+#     ["CCc1ccccc1", {0: False, 1: False, 2: True, 3: True, 4: True, 5: True, 6: True, 7: True}],  # ethyl benzene
+#     ["C=Cc1ccccc1", {0: True, 1: True, 2: True, 3: True, 4: True, 5: True, 6: True, 7: True}],  # styrene
+#     ["C=CC=C", {0: True, 1: True, 2: True}],  # butadiene
+#     ["C=CC(C)=C", {0: True, 1: True, 2: False, 3: True}],  # isoprene
+#     ["C=COC=C", {0: True, 1: True, 2: True, 3: True}],
+#     ["CC=COC=C", {0: False, 1: True, 2: True, 3: True, 4: True}],
+#     ["C=C[C+]C=C", {0: True, 1: True, 2: True, 3: True}],
+#     ["C=C[C-]C=C", {0: True, 1: True, 2: True, 3: True}],
+#     ["c1ccc(cc1)/C=C/C=O",  {0: True, 1: True, 2: True, 3: True, 4: True, 5: True, 6: True, 7: True}]
+# c1ccc2cc3ccccc3cc2c1  # anthacene
+#
+#     # negative
+#     ["C=CCC=C", {0: False, 1: False, 2: False, 3: False}],
+# ]
+#
+#
+# @pytest.mark.parametrize("case", case_for_conjugated)
+# def test_incomplete_conjugated(case: list):
+#     input_, answer = case
+#     result = bigsmiles.BigSMILES(input_)
+#     for k, v in answer.items():
+#         assert result.bonds[k].conjugated == v
 
-"""
-
-N1=NN=C[N]1 
-[CH2]C=C 
-[CH]C=C 
-[CH2]CCCC
-
-test for radicals 
 
-"""
\ No newline at end of file
+#    "OCCn2c(=N)n(CCOc1ccc(Cl)cc1Cl)c3ccccc23", Too many bonds to n, c, n
+# "s1ccsc2=c1c(=O)c1c(c2=O)cccc1"  # ambigous aromatics O=C(C1=C(C=CC=C1)C2=O)C3=C2SC=CS3
+# "OCCN2C(=N)N(CCOC1=CC=C(Cl)C=C1Cl)C3=CC=CC=C23",
diff --git a/tests/bigsmiles/test_constructor.py b/tests/bigsmiles/test_constructor.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/bigsmiles/test_reaction.py b/tests/bigsmiles/test_reaction.py
new file mode 100644
index 0000000..388d6fd
--- /dev/null
+++ b/tests/bigsmiles/test_reaction.py
@@ -0,0 +1,75 @@
+
+import pytest
+
+import bigsmiles
+
+
+bigsmiles.Config.show_aromatic_bond = False
+
+test_rxn = [
+    # no agent
+    ["C=CCBr >> C=CCI", [[bigsmiles.BigSMILES("C=CCBr")], [], [bigsmiles.BigSMILES("C=CCI")]]],
+    ["CC(=O)[OH]>>CC(=O)OCC", [[bigsmiles.BigSMILES("CC(=O)[OH]")], [], [bigsmiles.BigSMILES("CC(=O)OCC")]]],
+
+    # agent
+    ["C1=CC=CC=C1C(O)C(C)NC>SO(Cl)Cl>C1=CC=CC=C1C(Cl)C(C)NC",
+     [[bigsmiles.BigSMILES("C1=CC=CC=C1C(O)C(C)NC")],
+      [bigsmiles.BigSMILES("SO(Cl)Cl")],
+      [bigsmiles.BigSMILES("C1=CC=CC=C1C(Cl)C(C)NC")]
+      ]
+     ],
+    ["C1=CC=CC=C1C(Cl)C(C)NC>[Pd],[H][H]>C1=CC=CC=C1CC(C)NC",
+     [[bigsmiles.BigSMILES("C1=CC=CC=C1C(Cl)C(C)NC")],
+      [bigsmiles.BigSMILES("[Pd]"), bigsmiles.BigSMILES("[H][H]")],
+      [bigsmiles.BigSMILES("C1=CC=CC=C1CC(C)NC")]
+      ]
+     ],
+
+
+    # polymer
+    ["C=C(C(=O)OC)>CCCCSC(=S)SC(C)C(=O)O,CS(=O)C>{[>][<]CC(C(=O)OC)[>][<]}",  # RAFT of MA in DMSO
+     [[bigsmiles.BigSMILES("C=C(C(=O)OC)")],
+      [bigsmiles.BigSMILES("CCCCSC(=S)SC(C)C(=O)O"), bigsmiles.BigSMILES("CS(=O)C")],
+      [bigsmiles.BigSMILES("{[>][<]CC(C(=O)OC)[>][<]}")]]
+     ],
+    ["C=Cc1ccccc1.C[CH-](.[Li+])CC>Cc1ccccc1>CC(CC){[>][<]CC(c1ccccc1)[>][<]}[H]",
+     [
+         [bigsmiles.BigSMILES("C=Cc1ccccc1"), bigsmiles.BigSMILES("C[CH-](.[Li+])CC")],
+         [bigsmiles.BigSMILES("Cc1ccccc1")],
+         [bigsmiles.BigSMILES("CC(CC){[>][<]CC(c1ccccc1)[>][<]}[H]")]
+     ]
+     ],  # anionic of styrene
+    ["OCCO,OC(=O)c1ccc(cc1)C(=O)O>>{[][<]OCCO[<],[>]C(=O)c1ccc(cc1)C(=O)[>],[>][H],[<]O[]},O",
+     [
+         [bigsmiles.BigSMILES("OCCO"), bigsmiles.BigSMILES("OC(=O)c1ccc(cc1)C(=O)O")],
+         [],
+         [bigsmiles.BigSMILES("{[][<]OCCO[<],[>]C(=O)c1ccc(cc1)C(=O)[>],[>][H],[<]O[]}"), bigsmiles.BigSMILES("O")]
+     ]
+     ],  # PET synthesis
+
+    # atom index
+    ["[OH:1][CH2:2][CH2:3][OH:4],OC(=O)c1ccc(cc1)C(=O)O>>"
+     "{[][<][O:1][CH2:2][CH2:3][O:4][<],[>]C(=O)c1ccc(cc1)C(=O)[>],[>][H],[<]O[]},O",
+     [
+         [bigsmiles.BigSMILES("[OH:1][CH2:2][CH2:3][OH:4]"), bigsmiles.BigSMILES("OC(=O)c1ccc(cc1)C(=O)O")],
+         [],
+         [bigsmiles.BigSMILES("{[][<][O:1][CH2:2][CH2:3][O:4][<],[>]C(=O)c1ccc(cc1)C(=O)[>],[>][H],[<]O[]}"),
+          bigsmiles.BigSMILES("O")]
+     ]
+     ],  # PET synthesis
+]
+
+
+@pytest.mark.parametrize("case", test_rxn)
+def test_whole_system_molecule(case: str):
+    input_, answer = case
+    result = bigsmiles.Reaction(input_)
+    # reactants
+    for chem, ans in zip(result.reactants, answer[0]):
+        assert chem == ans
+    # agents
+    for chem, ans in zip(result.agents, answer[1]):
+        assert chem == ans
+    # products
+    for chem, ans in zip(result.products, answer[2]):
+        assert chem == ans
diff --git a/tests/bigsmiles/test_stereo_rules.py b/tests/bigsmiles/test_stereo_rules.py
new file mode 100644
index 0000000..70e4542
--- /dev/null
+++ b/tests/bigsmiles/test_stereo_rules.py
@@ -0,0 +1,73 @@
+
+import pytest
+
+import bigsmiles
+
+
+cases = [
+    ["F/C=C/F", {1: "E"}],  # E-difluoroethene
+    ["F\C=C\F", {1: "E"}],  # E-difluoroethene
+    ["F\C=C/F", {1: "Z"}],  # Z-difluoroethene
+    ["F/C=C\F", {1: "Z"}],  # Z-difluoroethene
+    ["C(\F)=C/F", {1: "E"}],  #
+    ["F/C(CC)=C/F", {3: "E"}],
+    ["CC(F)/C=C/F", {3: "E"}],
+    ["CCC(\F)=C/F", {3: "E"}],
+    ["CCC(\F)=C(/F)CC", {3: "E"}],
+    ["CCC(\F)=C(\F)CC", {3: "Z"}],
+    ["CCCCC(\CCCCF)=C(\CCCCF)CCCC", {9: "Z"}],
+    ["CCCC(C)C(\C(C)CCCF)=C(\CCCCF)CCCC", {11: "Z"}],
+    ["C(CCCC)C(\C(CCCF)C)=C(\CCCCF)CCCC", {11: "Z"}],
+    ["C/C(CC)=C(C(OCC)C(CF)C)/C(C(CC)C)OCC", {3: "E"}],
+    ["C/C(CC)=C(C(CCC)C(CF)C)/C(C(CC)C)=C/CC", {3: "Z", 17: "E"}],
+    ["F/C(C)=C(C(C(CC)F)CC(CCl)CBr)/C(C(Cl)CC)CC(CF)CBr", {2: "E"}],
+    ["F/C(C)=C(C(C(CC)F)OC(CCl)NBr)/C(C(CC)Cl)OC(CF)NBr", {2: "E"}],
+    ["F/C(C)=C(C(C(F)=C)OC(CCl)NBr)/C(CCl)OC(CF)NBr", {2: "E"}],
+    ["F/C(C)=C(C(C(F)=C)OC(CCl)NBr)/C(C(C)(C)F)OC(CF)NBr", {2: "Z"}],
+    ["F/C=C(CC(O)(O)OCC)\CC(OCC)=O", {2: "Z"}],
+    ["F/C=C(CC(O)(OC)OCC)\CC(OCC)=O", {2: "E"}],  # chemdraw E, rdkit Z
+    ["F/C=C(C/C(OCC)=N/C)/CC(NC)(OCC)NCC", {2, "Z"}],
+    ["F/C=C(C/C(OC)=N/CC)/CC(NC)(OCC)N(C)C", {2: "Z"}],  # chemdraw E, rdkit Z
+    ["F/C(C)=C(C(OC(NBr)CF)C(C)(F)C)\C(OC(C)C)C(F)=C", {2: "E"}],
+    ["F/C=C(CC(OC)(N(C)CC)N)\C/C(OC)=N/CC", {2: "E", 22: "Z"}],  # chemdraw E, rdkit Z (rdkit wrong)
+
+    # rings and bridges
+    ["FC1C2C3C/C(CCC3C2C1)=C4CC5CCC(F)C5C/4", {9: "Z"}],
+
+    # conjugated
+    ["F/C=C/C/C=C\C", {1: "E", 4: "Z"}],
+    ["F/C=C/CC=CC", {1: "E", 4: None}],  # partially specified
+
+    # Cumulene
+    ["F/C=C=C=C/F", {1: None, 2: "E", 3: None}],
+    ["F/C=C=C=C\F", {1: None, 2: "Z", 3: None}],
+    ["F/C(CC)=C=C=C\F", {3: None, 4: "Z", 5: None}],
+    ["F/C(CC)=C=C=C(CC)\F", {3: None, 4: "Z", 5: None}],
+    ["F/C(CC)=C=C=C(\CC)F", {3: None, 4: "E", 5: None}],
+
+    # double bonds
+    ["C(=C\F)(/C=C\C)\C=C\C", {1: "E", 5: "Z", 9: "E"}],
+    [r"C/1\2=C\F.C\1=C\C.C/2=C\C", {1: "E", 5: "Z", 9: "E"}],
+    ["C/C=C/C(/C=C\C)=C(/C=C/C)\C=C/C", {1: "E", 4: "Z", 6: "E", 8: "E", 11: "Z"}],
+
+]
+
+
+# @pytest.mark.parametrize("case", cases)
+# def test_ez_cases(case: str):
+#     input_, answer = case
+#     result = bigsmiles.BigSMILES(input_)
+#     for k in answer:
+#         assert result.bonds[k].double_bond_ez == answer[k]
+
+
+cases_errors = [
+    # missing '/'
+    "CC/C=CCC",
+    "CC/C=C",
+    "CC/C(\F)=C/F",
+    "F/C=C(/C=C/C)/C=C\C",  # two stereo symbols for first double bond right side
+    # terminal H
+    "CC/C=C/",
+]
+
diff --git a/tests/bigsmiles/test_syntax_fixes.py b/tests/bigsmiles/test_syntax_fixes.py
index db4418c..8e40196 100644
--- a/tests/bigsmiles/test_syntax_fixes.py
+++ b/tests/bigsmiles/test_syntax_fixes.py
@@ -1,7 +1,8 @@
 import pytest
 
-from bigsmiles import BigSMILES
+import bigsmiles
 
+bigsmiles.Config.show_aromatic_bond = False
 
 branch_cases = [
     # Branch syntax fixes
@@ -9,8 +10,8 @@
     ['CC(CC(CC))', 'CCCCCC'],
     ['CCCCCC()', 'CCCCCC'],
     ["C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C(C))))))))))))))))))))C", "C(CCCCCCCCCCCCCCCCCCCC)C"],
-    ["{[][$]CC(c1cc(C(=O)Oc2ccc(OCCCC)cc2)ccc1(C(=O)Oc3ccc(OCCCC)cc3))[$][$]}{[>][<]Si(C)(C)O[>][]}",
-     "{[][$]CC(c1cc(C(=O)Oc2ccc(OCCCC)cc2)ccc1C(=O)Oc3ccc(OCCCC)cc3)[$][$]}{[>][<]Si(C)(C)O[>][]}"],
+    ["{[][$]CC(c1cc(C(=O)Oc2ccc(OCCCC)cc2)ccc1(C(=O)Oc3ccc(OCCCC)cc3))[$][$]}{[>][<][Si](C)(C)O[>][]}",
+     "{[][$]CC(c1cc(C(=O)Oc2ccc(OCCCC)cc2)ccc1C(=O)Oc3ccc(OCCCC)cc3)[$][$]}{[>][<][Si](C)(C)O[>][]}"],
 ]
 
 
@@ -21,19 +22,17 @@ def test_branch_syntax_fixes(case):
     * bigsmiles.syntax_fixes.remove_unnecessary_branch_symbols() --> remove unnecessary branches
     * bigsmiles.constructor.BigSMILESConstructor.close_branch() --> remove empty branches
     """
-    result = BigSMILES(case[0])
+    result = bigsmiles.BigSMILES(case[0])
     assert str(result) == case[1]
 
 
 ring_cases = [
     # Branch syntax fixes
-    ['C12CCCC12C', 'C=1CCCC=1C'],
+    ['C12CCCC12C', 'C=1CCCC1C'],
     ['C2CCCC2C', 'C1CCCC1C'],
     ['CC(CC2CCC2)CC', 'CC(CC1CCC1)CC'],
     ['C%10CCCC%10C', 'C1CCCC1C'],
     ['C1C2C3C4C5C6C7C8C9C%10C%11OC1C2C3C4C5C6C7C8C9C%10C%11', 'C1C2C3C4C5C6C7C8C9C%10C%11OC1C2C3C4C5C6C7C8C9C%10C%11'],
-
-
 ]
 
 
@@ -41,10 +40,10 @@ def test_branch_syntax_fixes(case):
 def test_ring_syntax_fixes(case):
     """
     mainly testing the following functions:
-    * bigsmiles.constructor.BigSMILESConstructor.add_ring() --> two rings goes to a double bond
+    * bigsmiles.constructor.BigSMILESConstructor.add_ring_by_index() --> two rings goes to a double bond
     * bigsmiles.constructor.BigSMILESConstructor.add_ring_from_atoms() --> two rings goes to a double bond
     """
-    result = BigSMILES(case[0])
+    result = bigsmiles.BigSMILES(case[0])
     assert str(result) == case[1]
 
 
diff --git a/tests/bigsmiles/test_tokenizer.py b/tests/bigsmiles/test_tokenizer.py
index 3ce8434..c4cc242 100644
--- a/tests/bigsmiles/test_tokenizer.py
+++ b/tests/bigsmiles/test_tokenizer.py
@@ -1,96 +1,226 @@
+from __future__ import annotations
+
 import pytest
 
-from bigsmiles.tokenizer import TokenKind, tokenize, BigSMILESTokenizeError
+from bigsmiles.errors import TokenizeError
+from bigsmiles.constructors.tokenizer import TokenKind, tokenize, Token, tokenize_bonding_descriptor, \
+    tokenize_atom_symbol
+
+cases_for_atom_pattern = [
+    ["C", {"symbol": "C", "isotope": None, "stereo": None, "hydrogens": None, "charge": 0, "class_": None}],
+    ["Zn", {"symbol": "Zn", "isotope": None, "stereo": None, "hydrogens": None, "charge": 0, "class_": None}],
+    ["c", {"symbol": "c", "isotope": None, "stereo": None, "hydrogens": None, "charge": 0, "class_": None}],
+
+    # isotope
+    ["[13C]", {"symbol": "C", "isotope": 13, "stereo": None, "hydrogens": 0, "charge": 0, "class_": None}],
+    ["[238U]", {"symbol": "U", "isotope": 238, "stereo": None, "hydrogens": 0, "charge": 0, "class_": None}],
+
+    # stereo
+    ["[C@]", {"symbol": "C", "isotope": None, "stereo": "@", "hydrogens": 0, "charge": 0, "class_": None}],
+    ["[C@@]", {"symbol": "C", "isotope": None, "stereo": "@@", "hydrogens": 0, "charge": 0, "class_": None}],
+
+    # hydrogens
+    ["[H]", {"symbol": "H", "isotope": None, "stereo": None, "hydrogens": 0, "charge": 0, "class_": None}],
+    ["[C]", {"symbol": "C", "isotope": None, "stereo": None, "hydrogens": 0, "charge": 0, "class_": None}],
+    ["[CH]", {"symbol": "C", "isotope": None, "stereo": None, "hydrogens": 1, "charge": 0, "class_": None}],
+    ["[CH2]", {"symbol": "C", "isotope": None, "stereo": None, "hydrogens": 2, "charge": 0, "class_": None}],
+    ["[CH3]", {"symbol": "C", "isotope": None, "stereo": None, "hydrogens": 3, "charge": 0, "class_": None}],
+    ["[ZnH3]", {"symbol": "Zn", "isotope": None, "stereo": None, "hydrogens": 3, "charge": 0, "class_": None}],
+
+    # charge
+    ["[Fe+]", {"symbol": "Fe", "isotope": None, "stereo": None, "hydrogens": 0, "charge": 1, "class_": None}],
+    ["[Fe-]", {"symbol": "Fe", "isotope": None, "stereo": None, "hydrogens": 0, "charge": -1, "class_": None}],
+    ["[Fe+2]", {"symbol": "Fe", "isotope": None, "stereo": None, "hydrogens": 0, "charge": 2, "class_": None}],
+    ["[Fe+++]", {"symbol": "Fe", "isotope": None, "stereo": None, "hydrogens": 0, "charge": 3, "class_": None}],
+
+    # atom class_
+    ["[CH3:1]", {"symbol": "C", "isotope": None, "stereo": None, "hydrogens": 3, "charge": 0, "class_": 1}],
+    ["[Zn:53]", {"symbol": "Zn", "isotope": None, "stereo": None, "hydrogens": 0, "charge": 0, "class_": 53}],
+
+    # aromatic_elements
+    ["[13cH:1]", {"symbol": "c", "isotope": 13, "stereo": None, "hydrogens": 1, "charge": 0, "class_": 1}],
+
+]
+
+
+@pytest.mark.parametrize("case", cases_for_atom_pattern)
+def test_atom_tokenizer(case: list):
+    input_, answer = case
+    results = tokenize_atom_symbol(input_)
+    for key in answer:
+        assert results[key] == answer[key]
+
+
+cases_for_bonding_descriptor = [
+    [">", [">", 1]],
+    ["<", ["<", 1]],
+    ["$", ["$", 1]],
+    [">2", [">", 2]],
+    [">22", [">", 22]],
+
+    ["[>]", [">", 1]],
+    ["[<]", ["<", 1]],
+    ["[$]", ["$", 1]],
+    ["[>2]", [">", 2]],
+    ["[>22]", [">", 22]],
+]
+
+
+@pytest.mark.parametrize("case", cases_for_bonding_descriptor)
+def test_bonding_descriptor_tokenizer(case: list[str, list]):
+    input_, answer = case
+    results = tokenize_bonding_descriptor(input_)
+    for result, ans in zip(results, answer):
+        assert result == ans
+
 
-token_tests = [
+cases_for_full_tokenizer = [
     # Atoms
-    ["Zn", [TokenKind.Atom]],
-    ["C", [TokenKind.Atom]],
-    ["CCC", [TokenKind.Atom, TokenKind.Atom, TokenKind.Atom]],
-    ["c", [TokenKind.Aromatic]],
-    ["[C@]", [TokenKind.AtomExtend]],
-    ["[C@@]", [TokenKind.AtomExtend]],
-    ["[Fe+]", [TokenKind.AtomExtend]],
-    ["[Fe++]", [TokenKind.AtomExtend]],
-    ["[Fe+2]", [TokenKind.AtomExtend]],
-    ["[OH3+]", [TokenKind.AtomExtend]],
+    ["[Zn]", [Token(TokenKind.AtomExtend, "[Zn]")]],
+    ["C", [Token(TokenKind.Atom, "C")]],
+    ["CCC", [Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C")]],
+    ["c", [Token(TokenKind.Aromatic, "c")]],
+    ["[C@]", [Token(TokenKind.AtomExtend, "[C@]")]],
+    ["[C@@]", [Token(TokenKind.AtomExtend, "[C@@]")]],
+    ["[Fe+]", [Token(TokenKind.AtomExtend, "[Fe+]")]],
+    ["[Fe++]", [Token(TokenKind.AtomExtend, "[Fe++]")]],
+    ["[Fe+2]", [Token(TokenKind.AtomExtend, "[Fe+2]")]],
+    ["[OH3+]", [Token(TokenKind.AtomExtend, "[OH3+]")]],
 
     # Rings
-    ["C1C", [TokenKind.Atom, TokenKind.Ring, TokenKind.Atom]],
-    ["C7C", [TokenKind.Atom, TokenKind.Ring, TokenKind.Atom]],
-    ["C77C", [TokenKind.Atom, TokenKind.Ring, TokenKind.Ring, TokenKind.Atom]],
-    ["C%77C", [TokenKind.Atom, TokenKind.Ring2, TokenKind.Atom]],
+    ["C1C", [Token(TokenKind.Atom, "C"), Token(TokenKind.Ring, "1"), Token(TokenKind.Atom, "C")]],
+    ["C7C", [Token(TokenKind.Atom, "C"), Token(TokenKind.Ring, "7"), Token(TokenKind.Atom, "C")]],
+    ["C77C",
+     [Token(TokenKind.Atom, "C"), Token(TokenKind.Ring, "7"), Token(TokenKind.Ring, "7"), Token(TokenKind.Atom, "C")]],
+    ["C%77C", [Token(TokenKind.Atom, "C"), Token(TokenKind.Ring2, "%77"), Token(TokenKind.Atom, "C")]],
 
     # Bonds
-    ["C=C", [TokenKind.Atom, TokenKind.Bond, TokenKind.Atom]],
-    ["C#N", [TokenKind.Atom, TokenKind.Bond, TokenKind.Atom]],
-    ["C/C=C/C", [TokenKind.Atom, TokenKind.BondEZ, TokenKind.Atom, TokenKind.Bond, TokenKind.Atom,
-                 TokenKind.BondEZ, TokenKind.Atom]],
+    ["C=C", [Token(TokenKind.Atom, "C"), Token(TokenKind.Bond, "="), Token(TokenKind.Atom, "C")]],
+    ["C#N", [Token(TokenKind.Atom, "C"), Token(TokenKind.Bond, "#"), Token(TokenKind.Atom, "N")]],
+    ["C/C=C/C", [Token(TokenKind.Atom, "C"), Token(TokenKind.BondEZ, "/"), Token(TokenKind.Atom, "C"),
+                 Token(TokenKind.Bond, "="), Token(TokenKind.Atom, "C"), Token(TokenKind.BondEZ, "/"),
+                 Token(TokenKind.Atom, "C")]],
 
     # Branch
-    ["(", [TokenKind.BranchStart]],
-    [")", [TokenKind.BranchEnd]],
-    ["C(C)C", [TokenKind.Atom, TokenKind.BranchStart, TokenKind.Atom, TokenKind.BranchEnd, TokenKind.Atom]],
+    ["(", [Token(TokenKind.BranchStart, "(")]],
+    [")", [Token(TokenKind.BranchEnd, ")")]],
+    ["C(C)C", [Token(TokenKind.Atom, "C"), Token(TokenKind.BranchStart, "("), Token(TokenKind.Atom, "C"),
+               Token(TokenKind.BranchEnd, ")"), Token(TokenKind.Atom, "C")]],
 
     # Mixture
-    [".", [TokenKind.Mix]],
-    ["C.C", [TokenKind.Atom, TokenKind.Mix, TokenKind.Atom]],
+    [".", [Token(TokenKind.Disconnected, ".")]],
+    ["C.C", [Token(TokenKind.Atom, "C"), Token(TokenKind.Disconnected, "."), Token(TokenKind.Atom, "C")]],
 
     # Reaction
-    ["C>CC>>CCC", [TokenKind.Atom, TokenKind.Rxn, TokenKind.Atom, TokenKind.Atom, TokenKind.Rxn, TokenKind.Atom,
-                   TokenKind.Atom, TokenKind.Atom]],
+    ["C>CC>CCC", [Token(TokenKind.Atom, "C"), Token(TokenKind.Rxn, ">"), Token(TokenKind.Atom, "C"),
+                  Token(TokenKind.Atom, "C"), Token(TokenKind.Rxn, ">"), Token(TokenKind.Atom, "C"),
+                  Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C")]],
 
     # Bonding Descriptor
-    ["[>]", [TokenKind.BondDescriptor]],
-    ["[<]", [TokenKind.BondDescriptor]],
-    ["[$]", [TokenKind.BondDescriptor]],
-    ["[>1]", [TokenKind.BondDescriptor]],
-    ["[>12]", [TokenKind.BondDescriptor]],
-    ["[$]CCC(=[$])C[$]", [TokenKind.BondDescriptor, TokenKind.Atom, TokenKind.Atom, TokenKind.Atom,
-                      TokenKind.BranchStart, TokenKind.Bond, TokenKind.BondDescriptor, TokenKind.BranchEnd,
-                      TokenKind.Atom, TokenKind.BondDescriptor]],
+    ["[>]", [Token(TokenKind.BondDescriptor, "[>]")]],
+    ["[<]", [Token(TokenKind.BondDescriptor, "[<]")]],
+    ["[$]", [Token(TokenKind.BondDescriptor, "[$]")]],
+    ["[>1]", [Token(TokenKind.BondDescriptor, "[>1]")]],
+    ["[>12]", [Token(TokenKind.BondDescriptor, "[>12]")]],
+    ["[$]CCC(=[$])C[$]",
+     [Token(TokenKind.BondDescriptor, "[$]"), Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.BranchStart, "("), Token(TokenKind.Bond, "="),
+      Token(TokenKind.BondDescriptor, "[$]"), Token(TokenKind.BranchEnd, ")"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.BondDescriptor, "[$]")]],
     ["[H]O{[>][<]C(=O)CCCCC(=O)[<],[>]NCCCCCCN[>][<]}[H]",
-     [TokenKind.AtomExtend, TokenKind.Atom, TokenKind.StochasticStart, TokenKind.BondDescriptor,
-      TokenKind.BondDescriptor, TokenKind.Atom, TokenKind.BranchStart, TokenKind.Bond, TokenKind.Atom,
-      TokenKind.BranchEnd, TokenKind.Atom, TokenKind.Atom, TokenKind.Atom, TokenKind.Atom, TokenKind.Atom,
-      TokenKind.BranchStart, TokenKind.Bond, TokenKind.Atom, TokenKind.BranchEnd, TokenKind.BondDescriptor,
-      TokenKind.StochasticSeperator, TokenKind.BondDescriptor, TokenKind.Atom, TokenKind.Atom, TokenKind.Atom,
-      TokenKind.Atom, TokenKind.Atom, TokenKind.Atom, TokenKind.Atom, TokenKind.Atom, TokenKind.BondDescriptor,
-      TokenKind.BondDescriptor, TokenKind.StochasticEnd, TokenKind.AtomExtend]],
+     [Token(TokenKind.AtomExtend, "[H]"), Token(TokenKind.Atom, "O"), Token(TokenKind.StochasticStart, "{"),
+      Token(TokenKind.BondDescriptor, "[>]"), Token(TokenKind.BondDescriptor, "[<]"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.BranchStart, "("), Token(TokenKind.Bond, "="), Token(TokenKind.Atom, "O"),
+      Token(TokenKind.BranchEnd, ")"), Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.BranchStart, "("), Token(TokenKind.Bond, "="), Token(TokenKind.Atom, "O"),
+      Token(TokenKind.BranchEnd, ")"), Token(TokenKind.BondDescriptor, "[<]"),
+      Token(TokenKind.StochasticSeperator, ","),
+      Token(TokenKind.BondDescriptor, "[>]"), Token(TokenKind.Atom, "N"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "N"), Token(TokenKind.BondDescriptor, "[>]"),
+      Token(TokenKind.BondDescriptor, "[<]"), Token(TokenKind.StochasticEnd, "}"), Token(TokenKind.AtomExtend, "[H]")]],
 
     # ladder
-    ["[$1[$2]1]", [TokenKind.BondDescriptorLadder]],
+    ["[$1[$2]1]", [Token(TokenKind.BondDescriptorLadder, "[$1[$2]1]")]],
     ["{[]CCC([$1[$1]1])CC(OC[$1[$2]1])(CC[$1[$1]2])OC[$1[$2]2][]}",
-     [TokenKind.StochasticStart, TokenKind.ImplictEndGroup, TokenKind.Atom, TokenKind.Atom, TokenKind.Atom,
-      TokenKind.BranchStart, TokenKind.BondDescriptorLadder, TokenKind.BranchEnd, TokenKind.Atom, TokenKind.Atom,
-      TokenKind.BranchStart, TokenKind.Atom, TokenKind.Atom, TokenKind.BondDescriptorLadder, TokenKind.BranchEnd,
-      TokenKind.BranchStart, TokenKind.Atom, TokenKind.Atom, TokenKind.BondDescriptorLadder, TokenKind.BranchEnd,
-      TokenKind.Atom, TokenKind.Atom, TokenKind.BondDescriptorLadder, TokenKind.ImplictEndGroup,
-      TokenKind.StochasticEnd]],
+     [Token(TokenKind.StochasticStart, "{"), Token(TokenKind.ImplictEndGroup, "[]"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"), Token(TokenKind.BranchStart, "("),
+      Token(TokenKind.BondDescriptorLadder, "[$1[$1]1]"), Token(TokenKind.BranchEnd, ")"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.BranchStart, "("), Token(TokenKind.Atom, "O"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.BondDescriptorLadder, "[$1[$2]1]"), Token(TokenKind.BranchEnd, ")"),
+      Token(TokenKind.BranchStart, "("), Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.BondDescriptorLadder, "[$1[$1]2]"), Token(TokenKind.BranchEnd, ")"), Token(TokenKind.Atom, "O"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.BondDescriptorLadder, "[$1[$2]2]"),
+      Token(TokenKind.ImplictEndGroup, "[]"), Token(TokenKind.StochasticEnd, "}")]],
 
     # stochastic symbols
-    ["C,C", [TokenKind.Atom, TokenKind.StochasticSeperator, TokenKind.Atom]],
-    ["C;C", [TokenKind.Atom, TokenKind.StochasticSeperator, TokenKind.Atom]],
-    ["C{C", [TokenKind.Atom, TokenKind.StochasticStart, TokenKind.Atom]],
-    ["C}C", [TokenKind.Atom, TokenKind.StochasticEnd, TokenKind.Atom]],
+    ["C,C", [Token(TokenKind.Atom, "C"), Token(TokenKind.StochasticSeperator, ","), Token(TokenKind.Atom, "C")]],
+    ["C;C", [Token(TokenKind.Atom, "C"), Token(TokenKind.StochasticSeperator, ";"), Token(TokenKind.Atom, "C")]],
+    ["C{C", [Token(TokenKind.Atom, "C"), Token(TokenKind.StochasticStart, "{"), Token(TokenKind.Atom, "C")]],
+    ["C}C", [Token(TokenKind.Atom, "C"), Token(TokenKind.StochasticEnd, "}"), Token(TokenKind.Atom, "C")]],
+
+    # Atom class
+    ["[CH3:1][CH2:2][CH2:3][CH2:4][CH2:5][CH3:6]",
+     [Token(TokenKind.AtomExtend, "[CH3:1]"), Token(TokenKind.AtomExtend, "[CH2:2]"),
+      Token(TokenKind.AtomExtend, "[CH2:3]"), Token(TokenKind.AtomExtend, "[CH2:4]"),
+      Token(TokenKind.AtomExtend, "[CH2:5]"), Token(TokenKind.AtomExtend, "[CH3:6]")]],
+    ["[CH3:1]CC[CH2:2][CH2:3][CH3:4]",
+     [Token(TokenKind.AtomExtend, "[CH3:1]"), Token(TokenKind.Atom, "C"),
+      Token(TokenKind.Atom, "C"), Token(TokenKind.AtomExtend, "[CH2:2]"),
+      Token(TokenKind.AtomExtend, "[CH2:3]"), Token(TokenKind.AtomExtend, "[CH3:4]")]],
+
+    # aromatic_elements
+    ["c", [Token(TokenKind.Aromatic, "c")]],
+    ["c1ccccc1", [Token(TokenKind.Aromatic, "c"), Token(TokenKind.Ring, "1"), Token(TokenKind.Aromatic, "c"),
+                  Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"),
+                  Token(TokenKind.Aromatic, "c"), Token(TokenKind.Ring, "1")]],
+    ["c1[13cH]cc[c+]c1", [Token(TokenKind.Aromatic, "c"), Token(TokenKind.Ring, "1"),
+                          Token(TokenKind.AtomExtend, "[13cH]"), Token(TokenKind.Aromatic, "c"),
+                          Token(TokenKind.Aromatic, "c"), Token(TokenKind.AtomExtend, "[c+]"),
+                          Token(TokenKind.Aromatic, "c"), Token(TokenKind.Ring, "1")]],
+    ["c1ccccc1-c2ccccc2", [
+        Token(TokenKind.Aromatic, "c"), Token(TokenKind.Ring, "1"), Token(TokenKind.Aromatic, "c"),
+        Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"),
+        Token(TokenKind.Aromatic, "c"), Token(TokenKind.Ring, "1"), Token(TokenKind.Bond, "-"),
+        Token(TokenKind.Aromatic, "c"), Token(TokenKind.Ring, "2"), Token(TokenKind.Aromatic, "c"),
+        Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"),
+        Token(TokenKind.Aromatic, "c"), Token(TokenKind.Ring, "2")
+    ]],
+    ["OCCn2c(=N)nc3ccccc23", [
+        Token(TokenKind.Atom, "O"), Token(TokenKind.Atom, "C"), Token(TokenKind.Atom, "C"),
+        Token(TokenKind.Aromatic, "n"), Token(TokenKind.Ring, "2"), Token(TokenKind.Aromatic, "c"),
+        Token(TokenKind.BranchStart, "("), Token(TokenKind.Bond, "="), Token(TokenKind.Atom, "N"),
+        Token(TokenKind.BranchEnd, ")"), Token(TokenKind.Aromatic, "n"), Token(TokenKind.Aromatic, "c"),
+        Token(TokenKind.Ring, "3"), Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"),
+        Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"), Token(TokenKind.Aromatic, "c"),
+        Token(TokenKind.Ring, "2"), Token(TokenKind.Ring, "3")
+
+    ]],
 
 ]
 
 
-@pytest.mark.parametrize("test_case", token_tests)
-def test_tokenizer(test_case: list):
-    results = tokenize(test_case[0])
-    assert all([result.kind == answer for result, answer in zip(results, test_case[1])])
+@pytest.mark.parametrize("case", cases_for_full_tokenizer)
+def test_tokenizer(case: list):
+    input_, answer = case
+    results = tokenize(input_)
+    for result, ans in zip(results, answer):
+        assert result == ans
 
 
 negative_token_tests = [
-    "E",  # not an element
+    "Zn",  # not in brackets
+    "E",  # not an symbol
     "%",  # not valid on its own
     "[C<]",  # not valid
+    "[CH2:]",
+    "[:3]",
+    "[12]"
 ]
 
 
 @pytest.mark.parametrize("test_case", negative_token_tests)
 def test_tokenizer_error(test_case: str):
-    with pytest.raises(BigSMILESTokenizeError) as p:
+    with pytest.raises(TokenizeError) as p:
         tokenize(test_case)
diff --git a/tests/bigsmiles/test_validation_string.py b/tests/bigsmiles/test_validation_string.py
new file mode 100644
index 0000000..34d9f68
--- /dev/null
+++ b/tests/bigsmiles/test_validation_string.py
@@ -0,0 +1,34 @@
+import pytest
+
+import bigsmiles.errors as errors
+import bigsmiles.validation.validation_string as validation
+
+ring_renumbering_cases = [
+    ["O1CCCCC1N1CCCCC1", "O1CCCCC1N2CCCCC2"],
+    ["O1CC1CC1C1N1CCCCC1", "O1CC1CC2C2N3CCCCC3"],
+    ["[12C]C1=C(C(=NC=C1C=O)C)O", "[12C]C1=C(C(=NC=C1C=O)C)O"],  # no change
+    ["C1C(C)([CH7])[N+7]([7C-4])C1(=O)C14C3C2C1C5C2C3C45C(=O)C69C8[$1]C7C6C%10C7C8C9%10[>7]",
+     "C1C(C)([CH7])[N+7]([7C-4])C1(=O)C%114C3C2C%11C5C2C3C45C(=O)C69C8[$1]C7C6C%10C7C8C9%10[>7]"]
+]
+
+
+@pytest.mark.parametrize("case", ring_renumbering_cases)
+def test_ring_renumbering(case):
+    input_, answer = case
+    result = validation.validate_ring_numbering(input_, True)
+    assert result == answer
+
+
+ring_renumbering_cases_error = [
+    ["O1CCCCC1N1CCCCC1", "O1CCCCC1N2CCCCC2"],
+    ["O1CC1CC1C1N1CCCCC1", "O1CC1CC2C2N3CCCCC3"],
+    ["C1C(C)([CH7])[N+7]([7C-4])C1(=O)C14C3C2C1C5C2C3C45C(=O)C69C8[$1]C7C6C%10C7C8C9%10[>7]",
+     "C1C(C)([CH7])[N+7]([7C-4])C1(=O)C%114C3C2C%11C5C2C3C45C(=O)C69C8[$1]C7C6C%10C7C8C9%10[>7]"]
+]
+
+
+@pytest.mark.parametrize("case", ring_renumbering_cases_error)
+def test_ring_renumbering_error(case):
+    input_, answer = case
+    with pytest.raises(errors.BigSMILESError):
+        validation.validate_ring_numbering(input_, False)
diff --git a/tests/performance_analysis/main.py b/tests/performance_analysis/main.py
index 37463d6..c0e3ca5 100644
--- a/tests/performance_analysis/main.py
+++ b/tests/performance_analysis/main.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import sys
 import time
 import datetime
@@ -126,11 +128,12 @@ def main():
     bigsmiles_graph_time = 0 #time_bigsmiles_parsing_graph(polymer_string, time_iter)
     bigsmiles_graph_memory = 0# memory_bigsmiles_graph(polymer_string)
 
+    python_ = sys.version_info
     titles = "date/time (UTF), package version, time per parse (us), memory usage per bigsmiles (bytes), " \
-             "platform.processor, graph time (us), memory usage per graph (bytes)"
+             "platform.processor, graph time (us), memory usage per graph (bytes)", "python version", "notes"
     row = f"\n{datetime.datetime.utcnow()}, {bigsmiles.__version__}, {bigsmiles_parse_time:2.5f}, " \
           f"{bigsmiles_parse_memory:2.0f}, {platform.processor().replace(',', '')}, {bigsmiles_graph_time:2.5f}, " \
-          f"{bigsmiles_graph_memory:2.0f}"
+          f"{bigsmiles_graph_memory:2.0f}, {python_.major}.{python_.minor}.{python_.micro}, "
 
     print(titles, row)
     with open("performance.csv", "a", encoding="UTF-8") as file:
diff --git a/tests/performance_analysis/performance.csv b/tests/performance_analysis/performance.csv
index a5a7a69..447dee0 100644
--- a/tests/performance_analysis/performance.csv
+++ b/tests/performance_analysis/performance.csv
@@ -1,21 +1,37 @@
-date/time (UTF), package version, time per parse (us), memory usage per bigsmiles (bytes), platform.processor, bigsmiles_graph_time, bigsmiles_graph_memory,notes
-2022-12-13 15:51:49.564455, 0.0.1, 0.33659, 6275, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel,
-2022-12-13 15:52:02.381073, 0.0.1, 0.31270, 6275, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel,
-2022-12-13 16:01:46.489528, 0.0.1, 0.28784, 6275, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel,
-2022-12-13 16:03:14.753212, 0.0.1, 0.27485, 6275, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel,
-2022-12-13 21:31:23.009523, 0.0.2, 0.31450, 5702, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel,
-2022-12-13 21:32:01.900715, 0.0.2, 0.30643, 5702, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel,
-2022-12-13 22:13:58.425217, 0.0.2, 0.33733, 5702, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel,
-2022-12-14 01:06:36.558137, 0.0.1, 0.18101, 5702, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel,
-2022-12-22 00:00:20.407052, 0.0.3, 0.19951, 6675, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel,
-2022-12-26 04:32:43.516168, 0.0.3, 0.20379, 7184, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 2.72176, 41182,
-2022-12-27 01:00:02.983882, 0.0.3, 0.20270, 7184, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 1.97956, 38293,
-2023-02-17 17:40:07.090952, 0.0.4, 0.37197, 7996, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0,
-2023-02-17 18:48:28.533035, 0.0.4, 0.37409, 7855, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0,
-2023-02-17 18:49:10.168724, 0.0.4, 0.37617, 7855, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0,
-2023-02-17 20:55:42.411496, 0.0.3, 0.40347, 7192, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0,
-2023-02-17 20:56:46.349123, 0.0.3, 0.30584, 7192, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0,
-2023-02-18 03:01:26.243019, 0.0.3, 0.18977, 7201, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0,
-2023-02-18 03:02:49.404505, 0.0.4, 0.18677, 7201, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0,
-2023-02-19 23:08:02.851348, 0.0.4, 0.19914, 12145, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0,adding __dict__ adds extra memory usage
-2023-02-19 23:12:55.757331, 0.0.4, 0.20133, 12145, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0,
+date/time (UTF), package version, time per parse (us), memory usage per bigsmiles (bytes), platform.processor, bigsmiles_graph_time, bigsmiles_graph_memory,python version,notes
+2022-12-13 15:51:49.564455, 0.0.1, 0.33659, 6275, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-13 15:52:02.381073, 0.0.1, 0.31270, 6275, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-13 16:01:46.489528, 0.0.1, 0.28784, 6275, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-13 16:03:14.753212, 0.0.1, 0.27485, 6275, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-13 21:31:23.009523, 0.0.2, 0.31450, 5702, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-13 21:32:01.900715, 0.0.2, 0.30643, 5702, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-13 22:13:58.425217, 0.0.2, 0.33733, 5702, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-14 01:06:36.558137, 0.0.1, 0.18101, 5702, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-22 00:00:20.407052, 0.0.3, 0.19951, 6675, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, null, null, 3.10.8, null
+2022-12-26 04:32:43.516168, 0.0.3, 0.20379, 7184, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 2.72176, 41182, 3.10.8, null
+2022-12-27 01:00:02.983882, 0.0.3, 0.20270, 7184, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 1.97956, 38293, 3.10.8, null
+2023-02-17 17:40:07.090952, 0.0.4, 0.37197, 7996, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-17 18:48:28.533035, 0.0.4, 0.37409, 7855, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-17 18:49:10.168724, 0.0.4, 0.37617, 7855, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-17 20:55:42.411496, 0.0.3, 0.40347, 7192, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-17 20:56:46.349123, 0.0.3, 0.30584, 7192, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-18 03:01:26.243019, 0.0.3, 0.18977, 7201, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-18 03:02:49.404505, 0.0.4, 0.18677, 7201, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-19 23:08:02.851348, 0.0.4, 0.19914, 12145, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,adding __dict__ adds extra memory usage
+2023-02-19 23:12:55.757331, 0.0.4, 0.20133, 12145, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-23 17:40:36.201323, 0.0.5, 0.21353, 12448, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,adding **kwargs and .parent to nodes
+2023-02-24 03:13:01.734571, 0.0.6, 0.15346, 10526, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,constructor class --> functions
+2023-02-24 15:43:12.961899, 0.0.6, 0.25632, 10526, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-24 20:44:35.205696, 0.0.6, 0.25858, 10526, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, null
+2023-02-26 17:02:02.085862, 0.0.6, 0.16431, 10526, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,additional validation added
+2023-02-27 01:04:25.477956, 0.0.6, 0.21264, 12606, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,adding bond descriptor validation
+2023-02-27 01:04:56.775891, 0.0.6, 0.21267, 12606, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,
+2023-02-27 13:52:15.778235, 0.0.6, 0.40802, 12596, Intel64 Family 6 Model 69 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,
+2023-02-28 13:38:33.911445, 0.0.6, 0.21319, 12606, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,
+2023-03-01 00:42:03.180376, 0.0.6, 0.24154, 12106, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,"adding more validation on valence, rings, bonding"
+2023-03-04 16:53:36.505618, 0.0.7, 0.24646, 13132, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8,"adding rxn, disconnect, atom index"
+2023-03-05 17:13:10.536754, 0.0.7, 0.24699, 13243, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8
+2023-03-05 20:54:35.824811, 0.0.7, 0.24869, 13266, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8
+2023-03-06 01:40:43.910454, 0.0.7, 0.23849, 13404, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8
+2023-03-07 00:24:49.258795, 0.0.7, 0.22070, 14527, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.7.16, 
+2023-03-07 00:25:22.937738, 0.0.7, 0.24791, 13404, Intel64 Family 6 Model 141 Stepping 1 GenuineIntel, 0.00000,  0, 3.10.8, 
\ No newline at end of file
diff --git a/tests/performance_analysis/profiling.py b/tests/performance_analysis/profiling.py
index 6f6615b..35160a4 100644
--- a/tests/performance_analysis/profiling.py
+++ b/tests/performance_analysis/profiling.py
@@ -38,7 +38,7 @@
 def main():
 
     for i in range(3_000):
-        bigsmiles.BigSMILES(polymers[i % len(polymers)]).graph()
+        bigsmiles.BigSMILES(polymers[i % len(polymers)])
 
 
 if __name__ == "__main__":
diff --git a/tests/performance_analysis/timing_function.py b/tests/performance_analysis/timing_function.py
new file mode 100644
index 0000000..d4ee6df
--- /dev/null
+++ b/tests/performance_analysis/timing_function.py
@@ -0,0 +1,36 @@
+import functools
+import time
+
+
+def timer(func):
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        start_time = time.perf_counter()
+        value = func(*args, **kwargs)
+        end_time = time.perf_counter()
+        run_time = end_time - start_time
+        print("Finished {} in {} secs".format(repr(func.__name__), round(run_time, 5)))
+        return value
+
+    return wrapper
+
+
+import bigsmiles
+
+n = 100000
+
+@timer
+def option1(a, n):
+    for _ in range(n):
+        b = a == a
+
+
+@timer
+def option2(a, n):
+    for _ in range(n):
+        b = a is a
+
+
+a = bigsmiles.BigSMILES("CCCC{[$][$]CC[$][$]}N(NSC)C")
+option1(a, n)
+option2(a, n)