from neurondm import *
# set predicates in the event that the default config options do not work
# if you cloned the NIF-Ontology into a nonstandard location change ontology_local_repo in devconfig.yaml
from pyontutils.namespaces import ilxtr as pred
from neurondm import phenotype_namespaces as phns

config = Config('neuron-lang-notebook')
# By default Config saves ontology files in NIF-Ontology/ttl/generated/neurons/
# and python files in pyontutils/neurondm/neurondm/compiled/

# NOTE: if you call config multiple times any call to Neuron
# will be associate with the most recent instance of Config

# you can ignore this cell
# some utility functions needed for this tutorial
# due to the potential for notebooks to run cells out of order
def cellguard(addns=False):
    # unfortunately ipy.hooks['pre_run_code_hook'].add(__cellguard)
    # causes this to be called too frequently :/
    setLocalNames()
    setLocalContext()
    if addns:
        setLocalNames(phns.BBP)

"""
Explanation: Neuron Lang is a python based DSL for naming neurons. Neurons are modelled as collections of phenotypes with semantics backed by Web Ontology Language (OWL2) classes. Neuron Lang provides tools for mapping to and from collections of local names for phenotypes by using ontology identifiers as the common language underlying all local naming. These tools also let us automatically generate names for neurons in a regular and consistent way using a set of rules operating on the neurons' constitutent phenotypes. Neuron Lang can export to python or to any serialziation supported by rdflib, however deterministic turtle (ttl) is prefered. Neuron Lang depends on files from the NIF-Ontology.
This notebook has examples of how to use Neuron Lang to:
* Define neurons and phenotypes.
* Export all defined neurons.
* Use %scig magic to search for existing ontology identifiers
* Use LocalNameManager to create abbreviations for phenotypes.
* Bind local names in the current python namespace using with or setLocalNames.
* Creat a phenotype context in which to define neurons using with or setLocalContext.
Please see the documentation in order to set up a working
environment for this notebook.
Setup for any file defining neurons
End of explanation
"""


myFirstNeuron = Neuron(Phenotype('NCBITaxon:10090'),
                       Phenotype('UBERON:0000955'))

# NOTE: label is cosmetic and will be overwritten by rdfs:label
# unless you set override=True

myPhenotype = Phenotype('NCBITaxon:9685',           # object
                        pred.hasInstanceInSpecies,  # predicate (optional)
                        label='Cat')                # label for human readability

# str and repr produce different results
print(myFirstNeuron)

print(repr(myFirstNeuron))  # NOTE: this is equivalent to typing `myFirstNeuron` and running the cell

"""
Explanation: Neurons
Neuron instances are build out of Phenotype instances.
Phenotypes are object-predicate pairs that take curied
string representations (or uris) as arguments.
End of explanation
"""


# view the turtle (ttl) serialization of all neurons
turtle = config.ttl()
print(turtle)

# view the python serialization of all neurons for the current config
python = config.python()
print(python)

# write the turtle file defined in cell 1
config.write()

# write a python file that has the same name as the file in cell 1
# but with python safe separators and a .py extension
config.write_python()

# view a list of all neurons for the current config
config.neurons()

"""
Explanation: Viewing and saving
Neuron Lang can only be used to add new neurons to a graph.
Therefore if you need to remove neruons you need to reset
the whole program. For this reason I do not suggest using
ipython notebooks since they persist state in ways that can
be very confusing when working with a persistent datastore.
End of explanation
"""


import neurondm.lang

%scig --help

# use -t to limit the number of results
%scig -t 1 t hippocampus -v

# you can escape spaces with \
%scig t macaca\ mulatta

# quotes also allow search with spaces
%scig -t 1 s 'nucleus basalis of meynert'

# without quotes scig will search multiple terms at once
%scig -t 1 t cat mouse

"""
Explanation: scig
When creating neurons we want to be able to find relevant identifiers
quickly while working. There is a cli utility called scig that can be
used as a cell magic %scig to search a SciGraph instance for terms.
End of explanation
"""


from pyontutils.scigraph_client import Graph, Vocabulary

sgg = Graph()
sgv = Vocabulary()

terms = sgv.findByTerm('neocortex')
nodes_edges = sgg.getNeighbors('UBERON:0000955', 
                               relationshipType='BFO:0000050',  # part of
                               direction='INCOMING')
print('synonyms:', terms[0]['synonyms'])
print('subjects:', *(e['sub'] for e in nodes_edges['edges']))

"""
Explanation: Accessing SciGraph directly from python
End of explanation
"""


from neurondm import LocalNameManager
from pyontutils.utils import TermColors as tc  # pretty printing that is not part of this tutorial

class myPhenotypeNames(LocalNameManager):  # see neurons.LocalNameManager
    Mouse = Phenotype('NCBITaxon:10090', pred.hasInstanceInSpecies)
    Rat = Phenotype('NCBITaxon:10116', pred.hasInstanceInSpecies)
    brain = Phenotype('UBERON:0000955', pred.hasSomaLocatedIn)
    PV = Phenotype('PR:000013502', pred.hasExpressionPhenotype)
    
# you can see all the mappings in a local name manager by printing it or repring it
print(myPhenotypeNames)

# with a context manager we can use a namespace to create neurons
# more concisely and more importantly to repr them more concisely

with myPhenotypeNames:
    n = Neuron(Rat, brain, PV)
    
    # printing is unaffected so the fully expanded form is always
    # accessible (__str__ vs __repr__)
    print(tc.red('print inside unchanged:'), n, sep='\n')
    
    print(tc.red('repr inside inside:'), repr(n))
    
    # we can also repr a neuron defined elsewhere using our own names
    
    print(tc.red('repr outside inside:'), repr(myFirstNeuron))

# outside the context manager our concise repr is gone
print(tc.red('repr inside outside:'), repr(n))

# in addition we will now get a NameError of we try to use bare words
try: Neuron(Rat)
except NameError: print(tc.blue('Rat fails as expected.'))

"""
Explanation: Namespaces - context managers
We can be more concise by creating a namespace for our phenotype names.
Normally these are defined in another file (e.g. phenotype_namespaces.py) so that they can be shared and reused.
NOTE: for a full explication of phenotype namespaces see neurondm/example.py
End of explanation
"""


cellguard()

# there are already many namespaces defined in phenotype_namespaces.py
print(tc.red('Namespaces:'), phns.__all__)

# setLocalNames adds any names from a namespace to the current namespace
setLocalNames(phns.Species)

# we can load additional names
setLocalNames(phns.Regions, phns.Layers)

# however we will get a ValueError on a conflict
try:
    setLocalNames(phns.Test)
except ValueError as e:
    print(tc.red('The error:'), e)

# we can extend namespaces as well (again, best in a separate file)
# as long as the local names match we can combine entries
class MoreSpecies(phns.Species, myPhenotypeNames):
    Cat = myPhenotype
    ACh = Phenotype('CHEBI:15355', pred.hasExpressionPhenotype)
    AChMinus = NegPhenotype(ACh)
    
with MoreSpecies:
    can = Neuron(Cat, ACh, L2)
    cant = Neuron(Cat, AChMinus, L3)
    print(tc.red('More species:'), can, cant, sep='\n')

# we can also refer to phenotypes in a namespace directly
n = Neuron(Mouse, MoreSpecies.ACh)
print(tc.red('Direct usage:'), n, sep='\n')

# getLocalNames can be used to inspect the current set of defined names
print(tc.red('getLocalNames:'), sorted(getLocalNames().keys()))

# clear the local names by calling setLocalNames with no arguments
setLocalNames()

# no more short names ;_;
try: Neuron(Mouse, PV)
except NameError: print(tc.blue('Neuron(Mouse, PV) fails as expected'))

# for the rest of these examples we will use the BBP namespace
setLocalNames(phns.BBP)


# define neurons using our local names
Neuron(Mouse, L23, CCK, NPY)
Neuron(Mouse, brain, L3, PV)
Neuron(PV, DA)

cellguard()

"""
Explanation: Namespaces 2 - global modification
End of explanation
"""


cellguard(True)

# we often want to create many neurons in the same contex
# the easiest way to do this is to use a instance of a neuron
# as the input to a context manager
with Neuron(Rat, CA1):
    n1 = Neuron(CCK)
    n2 = Neuron(NPY)
    n3 = Neuron(PC)

# neurons always retain the context they were created in
print(tc.red('example 1:'), *map(repr, (n1, n2, n3)), '', sep='\n')

# you cannot change a neuron's context but you can see its original context
print(tc.red('example 2:'), n3.context, '', sep='\n')
try:
    n3.context = Neuron(Mouse, CA2)
except TypeError as e:
    print(tc.red('error when setting context:'), e, '\n')

# you can also use with as syntax when creating a context
with Neuron(Mouse) as n4:
    n5 = Neuron(CCK)

print(tc.red('example 3:'), *map(repr, (n4, n5)), '', sep='\n')

# contexts cannot violate disjointness axioms
try:
    with Neuron(Rat):
        print(tc.red('neuron ok:'), Neuron(), '', sep='\n')
        with Neuron(Mouse):
            print('This will not print')
except TypeError: print(tc.blue('Neuron(Rat, Mouse) fails as expected\n'))

# if you define a new neuron inside a context it will carry
# that context with it if used to define a new context

# context does not nest for neurons defined outside a with

with n3:
    n6 = Neuron(VIP)
    with n5:                 # defined outside does not nest
        n7 = Neuron(SOM)
    with Neuron(SLM) as n8:  # defined inside nests
        n9 = Neuron(SOM)
    n10 = Neuron(SOM)

print(tc.red('example 4:'), *map(repr, (n3, n6, n5, n7, n8, n9, n10)), sep='\n')

# 
with Neuron(Rat), Neuron(CTX) as context:
    print(context)
    n11 = Neuron(L1)
    print(n11)

cellguard()

"""
Explanation: Context - context managers
End of explanation
"""


cellguard(True)

# like namespaces you can also set a persistent local context
context0 = Neuron(CCK, NPY, SOM, DA, CA1, SPy)
context1 = Neuron(Rat, S1, L4)
setLocalContext(context0)
print(tc.red('created with context:'), repr(Neuron(TPC)))

# contexts are addative
# to change context using a Neuron you need to setLocalContext() first

# without resetting we get a disjointness error
try: setLocalContext(Neuron(Rat, S1, L4))
except TypeError as e: print(tc.blue('Neuron(S1, CA1) fails as expected'), e)
    
# reset
setLocalContext()

# now we will not get an error
setLocalContext(Neuron(Rat, S1, L4))
print(tc.red('Success:'), repr(Neuron(PC)))

# a neuron declared in a different context can be used to change the context withour resetting
# if you know in advance that you will be dealing with multiple contexts, I suggest you
# create all of those context neurons first so that they are available when needed
setLocalContext(context0)

# like namespaces call getLocalContext to see the current context
print(tc.red('getLocalContext:'), *(p.pShortName for p in getLocalContext()))

# like namespaces calling setLocalContext without arguments clears context
setLocalContext()
print(tc.red('no context:'), repr(Neuron(brain)))

cellguard()

"""
Explanation: Context 2 - global modification
End of explanation
"""


cellguard(True)

context = (Rat, S1)
ca1_context = (Rat, CA1)

def NeuronC(*args, **kwargs):
    return Neuron(*args, *context, **kwargs)

def NeuronH(*args, **kwargs):
    return Neuron(*args, *ca1_context, **kwargs)

neurons = {
    'HBP_CELL:0000013': NeuronC(CCK),
    'HBP_CELL:0000016': NeuronC(PV),
    'HBP_CELL:0000018': NeuronC(PC),
    'HBP_CELL:0000135': NeuronH(SLM, PPA),
    'HBP_CELL:0000136': NeuronH(SO, BP),
    'HBP_CELL:0000137': NeuronH(SPy, BS),
    'HBP_CELL:0000148': Neuron(Rat, STRI, MSN, D1),
    'HBP_CELL:0000149': Neuron(Rat, CA3, PC),
        }
neurons['HBP_CELL:0000013']

cellguard()

"""
Explanation: Context 3 - the old way
End of explanation
"""


cellguard(True)

try: Neuron(Mouse, Rat)
except TypeError as e: print(tc.blue('Neuron(Mouse, Rat) fails as expected'), e, sep='\n')
    
cellguard()

"""
Explanation: Disjointness
Neuron Lang enforces basic disjointness on phenotypes of 'data' level neurons
End of explanation
"""


