"""- Parse .ftbl
- Analyse ftbl

Restrictions:
 - metabolite name cannot have
   
   ":"
     it's a separator in measure id
   "+"
     in measurements it can be metab1+metab2+...
  
"""
            # # normalize or not?
            # collab=sorted(v for li in labs for v in li) # will be collapsed labels.
            # # the group is normalizable if collapsed labs is composed of only "x"
            # while True:
                # collab=sorted(collab) # if there are two collapsible labs, they will be neighbors
                # found=False
                # for i in range(len(collab)):
                    # if i == len(collab)-1:
                        # break
                    # s=np.array(list(collab[i])) if i == 0 else n # string
                    # n=np.array(list(collab[i+1])) # next
                    # cdif=s != n
                    # if cdif.sum() == 1:
                        # idif=np.where(cdif)[0][0]
                        # found=s[idif] == "0" and n[idif] == "1"
                        # if found:
                            # s[idif]="x"
                            # collab[i]="".join(s)
                            # del(collab[i+1])
                            # break
                # if not found or len(collab) == 1:
                    # break
            # if len(collab) == 1 and collab[0] == "x"*len(collab) and all(val == val):
                # s=sum(val)
                # val=val/s
                # sdv=sdv/s
                # norma=True
            # else:
                # norma=False

# 2008-01-22 sokol: ftbl_parse(f)
# 2008-01-25 sokol: ftbl_netan(ftbl)
# 2008-01-30 sokol: sto_r_m is added to the result of ftbl_netan()
# 2008-03-05 sokol: ftbl_parse(): added labeled atoms transitions in "TRANS" key
# 2008-03-05 sokol: ftbl_netan(): added isotopomers balance equations
# 2008-03-06 sokol: ftbl_netan(): added fwd-rev flux balance matrix
# 2008-03-10 sokol: ftbl_netan(): added carbon length
# 2008-03-?? sokol: ftbl_netan(): added chemical formula (reactions)
# 2008-03-?? sokol: ftbl_netan(): added cumomer balance pattern (cumo_balance_pattern)
# 2008-03-19 sokol: enum_metab(): metabolite list enumerated along to chemical pathways
# 2008-03-20 sokol: ftbl_netan(): added cumomer systems (cumo_sys)
# 2008-03-21 sokol: ftbl_netan(): added carbon transitions (carbotrans)
# 2008-03-25 sokol: iw(): added "index in given weight" iterator
# 2008-03-26 sokol: ftbl_netan(): added Cmax, maximal carbon length
# 2008-03-26 sokol: cumo_path(): enumerate cumomers of a given weight along pathways
# 2008-03-26 sokol: ftbl_netan(): removed metab_paths ordering
# 2008-03-26 sokol: ftbl_netan(): removed cumo_w ordering
# 2008-03-26 sokol: ftbl_netan(): added set of all metabolites (metabs)
# 2008-04-14 sokol: ftbl_netan(): added set of free fluxes (flux_free[net|xch])
# 2008-04-14 sokol: ftbl_netan(): added set of constrained fluxes flux_constr[net|xch])
# 2008-04-16 sokol: ftbl_netan(): added set of measured fluxes (flux_measured)
# 2008-04-18 sokol: ftbl_netan(): changed structure of sto_m_r, sto_r_m, flux_m_r, cumo_balance_pattern and matrix A
# 2008-04-18 sokol: ftbl_netan(): removed cumo_balance_pattern
# 2008-04-18 sokol: ftbl_netan(): cumomers matrix construction rewritten from scratch
# 2008-04-21 sokol: ftbl_netan(): added input cumomers to matrix
# 2008-04-21 sokol: ftbl_netan(): added input cumomers to netan (cumo_input)
# 2008-04-21 sokol: iso2cumo(): calculate cumo fraction from isotopomer ones
# 2008-06-12 sokol: ftbl_netan(): added inequality analysis
# 2008-06-12 sokol: ftbl_netan(): added label measurements analysis
# 2008-06-23 sokol: ftbl_netan(): added peak measurements analysis
# 2008-06-23 sokol: ftbl_netan(): added mass measurements analysis
# 2008-06-26 sokol: ftbl_netan(): added matrices for label (H1) and mass
# 2008-06-27 sokol: ftbl_netan(): added matrix for peaks (C13)
# 2008-07-18 sokol: ftbl_netan(): added cumomers ordered lists (one by weight)
# 2008-07-22 sokol: ftbl_netan(): added net and xch equalities
# 2008-07-23 sokol: ftbl_netan(): added Afl, bfl
# 2008-07-23 sokol: ftbl_netan(): added fluxes ordered lists (net and xch)
# 2008-07-24 sokol: ftbl_netan(): added free fluxes ordered lists
# 2008-07-24 sokol: ftbl_netan(): added constrained fluxes ordered lists
# 2008-07-24 sokol: ftbl_netan(): added measured fluxes ordered lists
# 2008-07-24 sokol: ftbl_netan(): added compleet fluxes ordered lists
# 2008-07-28 sokol: ftbl_netan(): added Afl row names (vrowAfl)
# 2008-07-28 sokol: ftbl_netan(): added in-out xch fluxes to constrained flux list
# 2008-07-30 sokol: ftbl_netan(): added in-out fluxes (flux_in, flux_out)
# 2008-09-01 sokol: labprods(): get labeled product by a given metabolite in a given reaction
# 2008-09-03 sokol: allprods(): get all labeled product by a given metabolite with others labeled isotops in a given reaction
# 2008-09-23 sokol: added mat2graph()
# 2008-10-10 sokol: added rcumo_sys()
# 2009-02-02 sokol: added iin_metab to output of cumo_infl()
# 2009-03-24 sokol: ftbl_netan(): added tmax, dt, metab_scale, met_pools
# 2009-03-27 sokol: ftbl_parse(): added float_conv field set
# 2009-05-28 sokol: added t_iso2m(): transition matrix from isotopomer vector to MID vector
# 2009-05-28 sokol: added t_iso2cumo(): transition matrix from isotopomer vector to cumomer vector
# 2009-05-28 sokol: added t_iso2pos(): transition matrix from isotopomer vector to positional labelling vector
# 2009-07-21 sokol: added conv_mid(): convolution of two mass isotopomer distribution
# 2009-09-14 sokol: Flux names changed flux.[net|xch] -> [dfc].[nx].flux
# 2009-10-19 sokol: ftbl_netan(): added consistancy check on fluxes, metabs and lengthes
# 2009-11-26 sokol: added infl(): set of incoming fluxes
# 2010-02-15 sokol: ftbl_parse(): fixed input/output fluxes with respect to d/f/c characterisation
# 2010-05-05 sokol: ftbl_parse(): if file name is given, check for .ftbl and add it if needed
# 2010-05-31 sokol: ftbl_parse(): non blocking errors for not complete ftbl (e.g. for graph plotting)
# 2010-10-11 sokol: ftbl_parse(): for measurements added row number for later identification
# 2010-10-11 sokol: ftbl_netan(): added "id" field in measures
# 2010-11-23 sokol: ftbl_netan(): added "nx2dfc" dictionary
# 2010-12-02 sokol: ftbl_netan(): added growth flux option
# 2011-04-20 sokol: cumo_infl(),...: not reversible fluxes are replaced by flux_out in cumomer balance
# 2011-04-27 sokol: ftbl_neta(): added flux_inout
# 2011-04-27 sokol: cumo_infl(),...: not reversible fluxes are replaced by flux_inout in cumomer balance
# 2012-05-04 sokol: added pooled metabs (same carbon length) in measurements
# 2012-06-25 sokol: added EMU fragment gathering ms_frag_gath()
# 2012-11-08 sokol: added variable growth fluxes depending on variable
#                   concentrations (flux_vgrowth, vflux_growth.[net])
# 2012-11-23 sokol: added concentration measurements
# 2014-01-22 sokol: added possibility of unknown fluxes in EQUALITY section
import numpy as np
import re
import copy
import os, sys
import math
from codecs import BOM_UTF8, BOM_UTF16_BE, BOM_UTF16_LE, BOM_UTF32_BE, BOM_UTF32_LE
from asteval import Interpreter
from functools import partial
import io
aeval=Interpreter(err_writer=io.TextIOWrapper(io.BytesIO()))

class oset(dict):
    def __init__(*args, **kwds):
        self, *args = args
        if len(args) == 1:
            for k in args[0]:
                self[k]=1
        elif len(args) > 1:
            raise TypeError('expected at most 1 arguments, got %d' % len(args))
    def copy(self):
        tmp=oset()
        tmp.update(self)
        return(tmp)
    def add(self, x):
        self[x]=1
    def difference_update(self, x):
        for k in x:
            if k in self:
                del(self[k])
    def difference(self, x):
        return(self - oset(x))
    def update(self, x):
        if x:
            for i in x:
                self[i]=1
    def intersection(self, x):
        return self & oset(x)
    def __sub__(self, x):
        tmp=oset(x)
        return oset(i for i in self if i not in tmp)
    def __and__(self, x):
        tmp=oset(x)
        return oset(i for i in self if i in tmp)
    def __or__(self, x):
        tmp=oset(x)
        tmp.update(self)
        return tmp

BOMS = (
    (BOM_UTF8, "UTF-8"),
    (BOM_UTF32_BE, "UTF-32-BE"),
    (BOM_UTF32_LE, "UTF-32-LE"),
    (BOM_UTF16_BE, "UTF-16-BE"),
    (BOM_UTF16_LE, "UTF-16-LE"),
)

try: werr
except NameError: werr = sys.stderr.write
try: wout
except NameError: wout = sys.stdout.write

me=os.path.realpath(sys.argv[0])
dirx=os.path.dirname(me)
sys.path.append(dirx)

from tools_ssg import *

NaN=float("nan")
NA=NaN
tol=sys.float_info.epsilon*2**7
float_conv=oset((
    "VALUE",
    "DEVIATION",
    "OPT_VALUE",
    "META_SIZE",
    "VALUE_S",
    "VALUE_D-",
    "VALUE_D+",
    "VALUE_DD",
    "VALUE(F/C)",
))
# define legal sections/subsections
defsec={
    "PROJECT": oset(),
    "NETWORK": oset(),
    "NOTRACER_NETWORK": oset(("FLUX_NAME", "EQUATION")),
    "FLUXES": oset(("NET", "XCH")),
    "EQUALITIES": oset(("NET", "XCH", "METAB")),
    "INEQUALITIES": oset(("NET", "XCH", "METAB")),
    "LABEL_INPUT": oset(),
    "FLUX_MEASUREMENTS": oset(),
    "LABEL_MEASUREMENTS": oset(),
    "PEAK_MEASUREMENTS": oset(),
    "MASS_SPECTROMETRY": oset(),
    "METAB_MEASUREMENTS": oset(),
    "OPTIONS": oset(),
    "METABOLITE_POOLS": oset(),
}
# at least one of these fields required to be in prl_exp ftbls
req_prl=(
    "LABEL_INPUT",
    "LABEL_MEASUREMENTS",
    "PEAK_MEASUREMENTS",
    "MASS_SPECTROMETRY"
)
if "ffguess" not in locals():
    ffguess=False
aeval.symtable=globals()
def eval_expr(e, werr=werr):
    res=aeval(e)
    if len(aeval.error) > 0:
        if werr:
            werr("In expression '"+str(e)+"' got errors:\n"+"\n".join(str(err.get_error()) for err in aeval.error)+"\n")
        return None
    return res
def ftbl_parse(f, wout=wout, werr=werr):
    """ftbl_parse(f) -> dict
    read and parse .ftbl file. The only input parameter f is a stream pointer
    with read permission or a file name.
    This function parses the input and returns a dictionnary
    with items corresponding to sections in .ftbl. One section is added.
    "TRANS" correponds to carbon transitions."""
    import re
    import codecs
    ftbl=dict();    # main dictionary to be returned
    
    #print("f=", f)
    if not isstr(f):
        Exception("parameter 'f' must be a string with FTBL file name")
    if f[-5:].lower() != ".ftbl":
        f=f+".ftbl"
    ftbl["name"]=f
    ftbl["base_name"]=os.path.basename(f)[:-5]
    ftbl["abs_path"]=os.path.abspath(f)
    fc=open(f, "rb")
    raw=fc.read()
    fc.close()
    co=[encoding for bom, encoding in BOMS if raw.startswith(bom)]
    if len(co):
        inp=raw.decode(co[0])
    else:
        for co in ["utf-8", "latin9", "utf-16", "utf-32"]:
            try:
                inp="".join(c for c in raw.decode(co) if c != '\x00')
                break;
            except UnicodeDecodeError:
                pass
    #pdb.set_trace()
    inp=inp.encode("utf-8").decode("utf-8-sig")
    lines=inp.splitlines()
    # fc=codecs.open(f, "r", encoding="utf-32")
    # try:
        # lines=fc.readlines()
        # fc.close()
    # except:
        # fc.close()
        # try:
            # fc=codecs.open(f, "r", encoding="utf-16")
            # lines=fc.readlines()
            # fc.close()
        # except:
            # fc.close()
            # try:
                # fc=codecs.open(f, "r", encoding="utf-8-sig")
                # lines=fc.readlines()
                # fc.close()
            # except:
                # fc.close()
                # fc=open(f, "r")
                # lines=fc.readlines()
                # fc.close()
    ftbl["pathway"]=dict()
    
    #print f;##
    reblank=re.compile(r"^[\t ]*$")
    recomm=re.compile(r"^[\t ]*//.*$")
    repath=re.compile(r"^[\t ]*//##[\t ]*(.*)[\t ]*$")
    comm=re.compile(r"^([^(//)]+|.+)//.*$")
    reading="sec_name"
    col_names=[]
    sec_name=subsec_name=""
    irow=0
    dic=dict()
    pathway=""
    #pdb.set_trace()
    for l in lines:
        irow+=1
        #print "raw l="+l;##
        # strip out \r
        l=l.replace("\r", "")
        if len(l) == 0:
            continue
        #print "-ctrl-r l="+l;##
        # strip out double quots
        if l[0] == '"': l=l[1:];    # very begining
        #print "-fq l="+l;##
        if l[-1] == '"': l=l[:-1];    # very end
        #print "-lq l="+l;##
        l=l.replace("\t\"", "\t");    # at the field begining
        #print "-ffq l="+l;##
        l=l.replace("\"\t", "\t");    # at the end
        #print "-lfq l="+l;##
        
        # check for pathway name "//## pathname"
        if repath.match(l):
            pathway=repath.sub(r"\1", l).rstrip()
        # skip comments and emty rows
        if recomm.match(l) or reblank.match(l): continue
        
        # skip the comments at the end of the row
        l=comm.sub(r"\1",l).rstrip()
        
        # split in fields
        flds=l.split("\t")
        
        #print "proceeding:"+l;##
        if len(flds) == 1:
            # new section starts here
            sec_name=flds[0]
            subsec_name=""
            if not sec_name in defsec:
                #pdb.set_trace()
                raise Exception("FTBL: Illegal section name '%s' (%s: %d)"%(sec_name, ftbl["name"], irow))
            # prepare storage
            ftbl[sec_name]=[]
            # prepare storage for carbon transitions
            if sec_name=="NETWORK":
               #print "TRANS prepared";##
               ftbl["TRANS"]=[]
            try: del stock
            except NameError: pass
            stock=ftbl[sec_name]
            skiptab=1
            data_count=0
            reading="col_names"
            col_names=[]
            subsec_name=""
            continue
        if len(flds) == 2 and len(flds[0]) == 0:
            # read subsection name or what ?
            if len(sec_name) and sec_name in defsec and len(defsec[sec_name]):
                # we are expecting a subsection
                subsec_name=flds[1]
                if subsec_name not in defsec[sec_name]:
                    raise Exception("A subsection '%s' cannot appear in the section '%s' (%s: %d)."%(subsec_name, sec_name, ftbl["name"], irow))
                # prepare sub-storage
                if not ftbl[sec_name]:
                    # replace an empty list by an empty dictionary
                    ftbl[sec_name]=dict()
                #print (irow, reading)##
                ftbl[sec_name][subsec_name]=[]
                try: del stock
                except NameError: pass
                stock=ftbl[sec_name][subsec_name]
                skiptab=2
                data_count=0
                reading="col_names"
                continue
            else:
                # just a very short line
                # it will fall in plain reading data
                pass
        if reading=="col_names" and len(flds) > 2:
            # read column names
            if len(l) < skiptab or l[:skiptab] != "\t"*skiptab:
                raise Exception("Expected at least %d tabulation(s) at the row beginning. Got '%s' (%s: %d)"%(skiptab, l[:min(skiptab, len(l))], ftbl["name"], irow))
            col_names=l[skiptab:].split("\t")
            if len([ item for item in col_names if re.match(r"^\s*$", item) ]):
                raise Exception("FTBL: row %d has empty column names:\n%s"%(irow,l))
            reading="data"
            #print "col_names=", col_names;##
            continue
        if reading=="data" or reading=="transitions":
            if len(l) < skiptab or l[:skiptab] != "\t"*skiptab:
                raise Exception("Expected at least %d tabulation(s) at the row beginning. Got '%s' (%s: %d)"%(skiptab, l[:min(skiptab, len(l))], ftbl["name"], irow))
            data=[it.strip() for it in l[skiptab:].split("\t")]
            prevdic=dic
            dic={"irow": str(irow)}
            if reading=="data" and sec_name == "NETWORK" and len(data[0]) != 0:
                reading="transitions"
            elif reading=="transitions":
                # here, we are at carbon transition line (e.g. #ABC -> #AB +#C)
                #print "data_count="+str(data_count), \
                #    "\ndata="+str([l for l in enumerate(data)]), \
                #    "\nstock="+str(stock);##
                if sec_name == "NETWORK" and len(data[0]) != 0:
                    raise Exception("Expected label transitions. Got '%s' (%s: %d)"%(l, ftbl["name"], irow))
                reading="data"
                fl_name=str(stock[data_count-1][col_names[0]]) if data_count else ""
                if col_names[0] not in prevdic or len(prevdic[col_names[0]]) == 0:
                    raise Exception("Carbon transition row '%s' is orphan (%s: %d)."%(l[skiptab:], ftbl["name"], irow))
                for i in range(len(col_names)):
                    item=data[i] if i < len(data) else ""
                    dic[col_names[i]]=item
                    metab=stock[data_count-1][col_names[i]]
                    if i > 0 and ((len(metab) and not len(item)) or (not len(metab) and len(item))):
                        #print "i=%d, co='%s', m='%s', tr='%s';"%(i, col_names[i], metab, item)
                        raise Exception("In the reaction '%s', metabolites are misaligned with carbon transitions (%s: %d)."%(fl_name, ftbl["name"], irow))
                ftbl["TRANS"].append(dic)
                continue
            for i in range(len(col_names)):
                # classic data
                if len(data) > len(col_names):
                    raise Exception("FTBL: data have more columns (%d) than column names (%d) (%s: %d)"%(len(data), len(col_names), ftbl["name"], irow))
                try:
                    # decimal point conversion
                    dic[col_names[i]]=data[i].strip() if i < len(data) else ""
                    if col_names[i] in float_conv:
                        oldval=dic[col_names[i]]
                        val=oldval.replace(",", ".")
                        try:
                            fval=float(val)
                            dic[col_names[i]]=val
                        except ValueError:
                            dic[col_names[i]]=oldval
                except IndexError:
                    pass
            #import pdb; pdb.set_trace()
            if sec_name == "NETWORK" and pathway:
                if pathway not in ftbl["pathway"]:
                    ftbl["pathway"][pathway]=[]
                ftbl["pathway"][pathway]+=[dic["FLUX_NAME"]]
            if sec_name == "FLUXES" and subsec_name in ("NET", "XCH") and dic["FCD"] in ("F", "C"):
                try:
                    val=float(eval_expr(dic["VALUE(F/C)"]))
                except (TypeError, ValueError):
                    raise ValueError("In the field 'VALUE(F/C)', a float value expected (%s: %d)"%(ftbl["name"], irow))
            stock.append(dic)
            data_count+=1
            #print "sec, subsec=", sec_name, subsec_name, data_count, dic;##
        #print "len(flds)=", len(flds), flds, l, data;##
        #print "keys", ftbl.keys(), (ftbl[sec_name].keys() \
        #        if type(ftbl[sec_name])==type({}) else "");##
    if "NETWORK" not in ftbl:
        return ftbl
    # prepare translator reac -> pathway
    ftbl["reac2path"]=dict((reac,path) for path,li in ftbl["pathway"].items() for reac in li)
    # assemble reactions with the same name in one
    # long_reac={reac: {"left": [(minp1, 1), (minp2, 1), ...], "right": [(mout1, 1), (mout2, 1), ...]}
    # and carbon transitions
    # long_trans={reac: {"left": [minp1, minp2, ...], "right": [mout1, mout2, ...]}
    # get unique reaction names
    nw=ftbl["NETWORK"]
    tr=ftbl["TRANS"]
    if (len(nw) != len(tr)):
        raise Exception("Number of reactions (%d) is not equal to label transition number (%d)"%(len(nw), len(tr)) )
    ureac=oset(row["FLUX_NAME"] for row in ftbl["NETWORK"])
    long_reac=dict()
    long_trans=dict()
    for reac in ureac:
        if reac not in long_reac:
            long_reac[reac]={"left": [], "right": []}
            long_trans[reac]={"left": [], "right": []}
        # get rows
        irows=[i for (i, row) in enumerate(nw) if row["FLUX_NAME"] == reac]
        if len(irows) > 1:
            # check that rows are contiguous
            if not all(r == i+irows[0] for i,r in enumerate(irows)):
                raise Exception("Reaction '%s' is split on non contiguous rows: %s"%(reac, ", ".join(nw[i]["irow"] for i in irows)))
        long_reac[reac]["irow"]=", ".join(str(nw[i]["irow"]) for i in irows)
        long_reac[reac]["left"]+=[mecoparse(m) for i in irows for m in (nw[i]["EDUCT_1"], nw[i]["EDUCT_2"]) if m]
        long_reac[reac]["right"]+=[mecoparse(m) for i in irows for m in (nw[i]["PRODUCT_1"], nw[i]["PRODUCT_2"]) if m]
        long_trans[reac]["irow"]=", ".join(str(tr[i]["irow"]) for i in irows)
        long_trans[reac]["left"]+=[m for i in irows for m in (tr[i]["EDUCT_1"], tr[i]["EDUCT_2"]) if m]
        long_trans[reac]["right"]+=[m for i in irows for m in (tr[i]["PRODUCT_1"], tr[i]["PRODUCT_2"]) if m]
        for lr in ("left", "right"):
            nmet=len(long_reac[reac][lr])
            ncarb=len(long_trans[reac][lr])
            if nmet != ncarb:
                raise Exception("Number of metabolites (%d) and carbon strings (%d) on the %s hand side of reaction 'reac' must be equal (%s: %s)"%(nmet, ncarb, lr, reac, ftbl["name"], long_reac[reac]["irow"]))
    ftbl["long_reac"]=long_reac
    ftbl["long_trans"]=long_trans
    return ftbl

def ftbl_netan(ftbl, netan, emu_framework=False, fullsys=False, case_i=False, wout=wout, werr=werr):
    """
    analyse ftbl dictionary to find
     
     - network inputs (input)
     - network outputs (output)
     - substrates (subs)
     - products (prods)
     - metabolites (metabs)
     - reactions (reacs)
     - not reversible reactions (subset of reacs) (notrev)
       all above items are in named sets
     - stocheometric matrix (sto_r_m)
     - stocheometric matrix (sto_m_r)
     - fwd-rev flux matrix (flux_m_r)
     - cumomer balances (cumo_m_r_m)
     - carbon length (Clen)
     - reaction formula (formula)
     - metabolite network (metab_netw)
     - carbon transitions (carbotrans)
     - free fluxes (flux_free)
     - constrained fluxes (flux_constr)
     - measured fluxes (flux_measured)
     - variable growth fluxes (flux_vgrowth)
     - input isotopomers (iso_input)
     - input isotopomers functions (funlab for case_i=True)
     - input cumomers (cumo_input)
     - input reduced cumomers (rcumo_input)
     - flux inequalities (flux_ineqal)
     - flux equalities (flux_eqal)
     - label measurements, H1 (label_meas)
     - peak measurements, C13 (peak_meas)
     - mass measurements (mass_meas)
     - cumomer ordered lists (vcumo)
     - unknown fluxes ordered lists (vflux)
     - linear problem on fluxes (Afl, bfl)
     - free fluxes ordered lists (vflux_free)
     - fw-rv fluxes ordered lists (vflux_fwrv)
     - row names ordered lists for Afl (vrowAfl)
     - in-out fluxes (flux_in, flux_out)
     - measured concentrations (metab_measured)
    """
    global eval_expr
    eval_expr=partial(eval_expr, werr=werr)
    #breakpoint()
    # init named sets
    if type(netan)!=type(dict()):
        raise TypeError("netan argument must be a dictionary")
    if not netan:
        netan.update({
            "input":oset(),
            "output":oset(),
            "deadend":oset(),
            "subs":oset(),
            "prods":oset(),
            "left":oset(),
            "right":oset(),
            "metabs":oset(),
            "reac":oset(),
            "notrev":oset(),
            "sto_r_m":dict(),
            "sto_m_r":dict(),
            "flux_m_r":dict(),
            "cumo_balance_pattern":dict(),
            "Clen":dict(),
            "formula":dict(),
            "metab_netw":dict(),
            "cumo_sys":dict(),
            "carbotrans":dict(),
            "Cmax":dict(),
            "flux_dep":dict(),
            "flux_free":dict(),
            "flux_constr":dict(),
            "flux_measured":dict(),
            "iso_input":[],
            "funlab":[],
            "cumo_input":[],
            "rcumo_input":[],
            "emu_input":[],
            "flux_inequal":{"net":[], "xch":[]},
            "flux_equal":{"net":[], "xch":[]},
            "label_meas":[],
            "peak_meas":[],
            "mass_meas":[],
            "vcumo":[],
            "Afl":[],
            "bfl":[],
            "vflux":{"net":[], "xch":[], "net2i":dict(), "xch2i":dict()},
            "vflux_free":{"net":[], "xch":[], "net2i":dict(), "xch2i":dict()},
            "vflux_constr":{"net":[], "xch":[], "net2i":dict(), "xch2i":dict()},
            "vflux_meas":{"net":[], "net2i":dict()},
            "vflux_growth":{"net":[], "net2i":dict()},
            "vflux_compl":{"net":[], "xch":[], "net2i":dict(), "xch2i":dict()},
            "vflux_fwrv":{"fw":[], "rv":[], "fw2i":dict(), "rv2i":dict()},
            "vrowAfl":[],
            "flux_in":oset(),
            "flux_out":oset(),
            "flux_inout":oset(),
            "opt":dict(),
            "met_pools":dict(),
            "nx2dfcg":dict(),
            "metab_measured":dict(),
        })
    netan["emu"]=emu_framework
    res="";     # auxiliary short-cut to current result
    netan["exp_names"]=[ftbl["base_name"]]
    netan["fullsys"]=fullsys
    netan["pathway"]=ftbl["pathway"]
    netan["reac2path"]=ftbl["reac2path"]

    # Isotopomer dynamics and other options
    for row in ftbl.get("OPTIONS",[]):
        #breakpoint()
        val=eval_expr(row["OPT_VALUE"], werr=None)
        netan["opt"][row["OPT_NAME"]]=row["OPT_VALUE"] if val is None else val
    #pdb.set_trace()
    for row in ftbl.get("METABOLITE_POOLS",[]):
        metab=row["META_NAME"]
        if metab in netan["met_pools"]:
            raise Exception("Metabolite '%s' is present more than once in the\nftbl secion METABOLITE_POOLS (second appearance on row %s)"%(metab, row["irow"]))
        netan["met_pools"][metab]=eval_expr(row["META_SIZE"])

    # check the presence of fields "NAME", "FCD" and maybe "VALUE(F/C)"
    for suf in ["NET", "XCH"]:
        for row in ftbl.get("FLUXES", dict()).get(suf, dict()):
            if not "NAME" in row:
                raise NameError("No required field NAME in section FLUX/%s (%s: %s)"%(suf, ftbl["name"], row["irow"]))
            if not "FCD" in row:
                raise NameError("No requied field FCD in section FLUX/%s (%s: %s)"%(suf, ftbl["name"], row["irow"]))
            if (row["FCD"]=="F" or row["FCD"]=="C") and not "VALUE(F/C)" in row:
                raise NameError("For flux '%s' in section FLUX/%s, the field 'VALUE(F/C)' is requiered but is absent (%s: %s)"%(row["NAME"], suf, ftbl["name"], row["irow"]))
    # quick consistency check between net and xch fluxes
    fnet=oset(row["NAME"] for row in ftbl.get("FLUXES", dict()).get("NET", dict()))
    fxch=oset(row["NAME"] for row in ftbl.get("FLUXES", dict()).get("XCH", dict()))
    net_wo_xch=fnet-fxch
    if net_wo_xch:
        raise Exception("Fluxe(s) '%s' are defined in the FLUX/NET section but not in the XCH one."%join(", ", net_wo_xch))
    xch_wo_net=fxch-fnet
    if xch_wo_net:
        raise Exception("Fluxe(s) '%s' are defined in the FLUX/XCH section but not in the NET one."%join(", ", xch_wo_net))

    # quick not reversible reactions for complete subs and prods accounting
    revreac=oset(row["NAME"] for row in ftbl.get("FLUXES", dict()).get("XCH", dict()) if row["FCD"]=="F" or (row["FCD"]=="C" and (eval_expr(row["VALUE(F/C)"]) != 0.)))
    # analyse networks
    netw=ftbl.get("long_reac")
    if not netw:
        raise Exception("No long_reac section in the ftbl parameter")
    row_to_del=[]
    for (reac, row) in netw.items():
        #print "reac="+reac;#
        # corresponding carbon transition row
        crow=ftbl["long_trans"][reac]
        # local substrate (es), product (ps) and metabolites (ms) sets
        es=oset(m for m,_ in row["left"])
        ps=oset(m for m,_ in row["right"])
        ms=es|ps
        #if es&ps:
        #   raise Exception("The same metabolite(s) '%s' are present in both sides of a reaction '%s' (%s: %s)."%(join(", ", es&ps), reac, ftbl["name"], row["irow"]))
        
        # all reactions A+B=C or C=A+B or A+B=C+D
        netan["reac"].add(reac)
        netan["left"].update(es)
        netan["right"].update(ps)
        netan["subs"].update(es)
        netan["prods"].update(ps)
        netan["metabs"].update(ms)
        if reac in revreac:
            netan["subs"].update(ps)
            netan["prods"].update(es)
        #aff("ms for "+reac, ms);##

        # create chemical formula
        netan["formula"][reac]={"left": row["left"], "right": row["right"], "all":ms}

        # Carbon length and transitions
        netan["carbotrans"][reac]={"left": [], "right": []}
        for (m, carb, lr) in [(row[lr][i][0], crow[lr][i], lr) for lr in ("left", "right") for i in range(len(row[lr]))]:
                #print "m="+str(m), "; carb="+str(carb);##
            if carb[0] != "#":
                raise Exception("In carbon string '%s' for metabolite '%s' a starting '#' is missing. (%s: %s)"%(carb, m, ftbl["name"], row["irow"]))
            # carbon transitions
            netan["carbotrans"][reac][lr].append((m,carb[1:])); # strip "#" character

            # carbon length
            if netan["Clen"].get(m, 0) and \
                    netan["Clen"][m] != len(carb)-1:
                raise Exception("CarbonLength", "Metabolite "+m+" has length "+
                        str(netan["Clen"][m])+" but in reaction "+reac+
                        " it has length "+str(len(carb)-1)+" (%s: %s)"%(ftbl["name"], row["irow"]))
            netan["Clen"][m]=len(carb)-1; # don't count '#' character
        # check equal carbon length on left and right sides
        lenl=sum(len(l) for m,l in netan["carbotrans"][reac]["left"])
        lenr=sum(len(l) for m,l in netan["carbotrans"][reac]["right"])
        if lenl!=lenr:
            raise Exception("Carbon patterns have different lengths on both sides of reaction '%s' (%d on left, %d on right, %s: %s)"%(reac, lenl, lenr, ftbl["name"], row["irow"]))
        # check unique presence of each letter on each side
        lets=dict()
        uni=dict()
        for lr in ("left", "right"):
            lets[lr]="".join(l for m,l in netan["carbotrans"][reac][lr])
            uni[lr]=oset(lets[lr])
            if len(uni[lr]) != len(lets[lr]):
                # find repeated letters
                di=dict((l,lets[lr].count(l)) for l in uni[lr])
                for (l,c) in di.items():
                    if c > 1:
                        raise Exception("Character '%s' is present %s on the %s side of carbon transition in reaction '%s' (%s: %s)"%(l, ntimes(c), lr, reac, ftbl["name"], row["irow"]))
        # check for perfect mapping
        lmr=uni["left"]-uni["right"]
        if lmr:
            raise Exception("Letter(s) '%s' are present on the left but not on the right hand side in carbon transitions for reaction '%s' (%s: %s)"%(", ".join(sorted(lmr)), reac, ftbl["name"], row["irow"]))
        rml=uni["right"]-uni["left"]
        if rml:
            raise Exception("Letter(s) '%s' are present on the right but not on the left hand side in carbon transitions for reaction '%s' (%s: %s)"%(", ".join(sorted(lmr)), reac, ftbl["name"], row["irow"]))

        # stocheometric matrix in dictionnary form
        # sto_r_m[reac]['left'|'right']=[(metab, coef)] and
        # sto_m_r[metab]['left'|'right']=list((reac, coef))
        # substrates are in 'left' list
        # and products are in the 'right' one.

        netan["sto_r_m"][reac]={"left":row["left"], "right":row["right"]}
        
        for s,c in netan["sto_r_m"][reac]["left"]:
            #print "sto_m_r s="+str(s);##
            if not s in netan["sto_m_r"]:
                netan["sto_m_r"][s]={"left":[], "right":[]}
            netan["sto_m_r"][s]["left"].append((reac, c))
        for s,c in netan["sto_r_m"][reac]["right"]:
            #print "sto_m_r s="+str(s)
            if not s in netan["sto_m_r"]:
                netan["sto_m_r"][s]={"left":[], "right":[]}
            netan["sto_m_r"][s]["right"].append((reac, c))
        # end netan["sto_m_r"]
        #aff("sto_m_r"+str(reac), netan["sto_m_r"]);##

    for row in ftbl.get("NOTRACER_NETWORK", []):
        reac=row["FLUX_NAME"]
        if reac in netan["sto_r_m"]:
            raise Exception("Reaction '%s' from NOTRACER_NETWORK section was already introduced in NETWORK section (%s: %s)."%(reac, ftbl["name"], row["irow"]))
        spl=row["EQUATION"].split("=")
        if len(spl) < 2:
            raise Exception("Not found '=' sign in NOTRACER_NETWORK reaction: '%s' (%s: %s)"%(reac, ftbl["name"], row["irow"]))
        elif len(spl) > 2:
            raise Exception("Too many '=' signs (%d) in NOTRACER_NETWORK reaction: '%s' (%s: %s)"%(reac, len(spl)-1, ftbl["name"], row["irow"]))
        lr=("left", "right")
        di=dict()
        for i,su in enumerate(spl):
            terms=su.split("+")
            try:
                di[lr[i]]=mecoparse(terms)
            except ValueError:
                werr("Error occured in '%s': %s\n"%(ftbl["name"], row["irow"]))
                raise
        netan["sto_r_m"][reac]=di
        for s,c in netan["sto_r_m"][reac]["left"]:
            #print "sto_m_r s="+str(s);##
            if not s in netan["sto_m_r"]:
                netan["sto_m_r"][s]={"left":[], "right":[]}
            netan["sto_m_r"][s]["left"].append((reac, c))
        for s,c in netan["sto_r_m"][reac]["right"]:
            #print "sto_m_r s="+str(s)
            if not s in netan["sto_m_r"]:
                netan["sto_m_r"][s]={"left":[], "right":[]}
            netan["sto_m_r"][s]["right"].append((reac, c))
        netan["reac"].add(reac)
        es=oset(m for m,co in di["left"])
        ps=oset(m for m,co in di["right"])
        ms=es|ps
        for m in ms:
            if m not in netan["Clen"]:
                netan["Clen"][m]=0
        netan["left"].update(es)
        netan["right"].update(ps)
        netan["subs"].update(es)
        netan["prods"].update(ps)
        netan["metabs"].update(ms)
        if reac in revreac:
            netan["subs"].update(ps)
            netan["prods"].update(es)
        #aff("ms for "+reac, ms);##

        # create chemical formula
        netan["formula"][reac]=di
        netan["formula"][reac]["all"]=ms

    # find input and output metabolites
    #pdb.set_trace()
    netan["input"].update(netan["subs"]-netan["prods"])
    netan["output"].update(netan["prods"]-netan["subs"])
    netan["deadend"].update(((netan["left"]-netan["right"]) | (netan["right"]-netan["left"])) - netan["input"]-netan["output"])
    # internal metabs
    netan["metabint"]=netan["metabs"].copy()
    netan["metabint"].difference_update(netan["input"] | netan["output"])
    netan["metab0len"]=set(m for m in netan["metabs"] if netan["Clen"].get(m, 0) == 0)

    # all met_pools must be in internal metabolites
    mdif=oset(netan["met_pools"]).difference(netan["metabint"])
    if len(mdif) :
        # unknown metabolite
        raise Exception("Unknown metabolite(s). Metabolite(s) '"+", ".join(mdif)+"' defined in section METABOLITE_POOLS are not internal metabolites in NETWORK section.")
    # all met_pools must carry labeling
    mcom=oset(netan["met_pools"]).intersection(netan["metab0len"])
    if len(mcom) :
        # unknown metabolite
        raise Exception("Non labeled specie in METABOLITE_POOLS. Metabolite(s) '"+"', '".join(mcom)+"' defined in section METABOLITE_POOLS are not carrying labeled atoms")
    if case_i:
        #pdb.set_trace()
        # check it other way: all metabint must be in metpools
        mdif=oset(netan["metabint"]-netan["metab0len"]).difference(netan["met_pools"])
        if len(mdif) :
            # unknown metabolite
            raise Exception("Unknown metabolite concentration. Metabolite(s) carrying labeling '"+", ".join(mdif)+"' defined in section NETWORK are not defined in METABOLITE_POOLS section.")

    # add growth fluxes if requested
    netan["flux_growth"]={"net":dict()}
    netan["flux_vgrowth"]={"net":dict(), "xch":dict()}; # fluxes depending on variable pools
    if netan["opt"].get("include_growth_flux"):
        if not netan["opt"].get("mu"):
            raise Exception("Parameter include_growth_flux is set to True but the growth parameter mu is absent or zero in OPTIONS section")
        for (m,si) in netan["met_pools"].items():
            mgr=m+"_gr"
            reac=mgr
            if reac in netan["reac"]:
                raise Exception("Cannot add growth reaction "+reac+". It is already in the network")
            netan["reac"].add(reac)
            netan["subs"].add(m)
            netan["prods"].add(mgr)
            netan["metabs"].add(m)
            netan["output"].add(mgr)
            if si > 0 :
               netan["flux_growth"]["net"][reac]=None
            else:
               netan["flux_vgrowth"]["net"][reac]=None
               netan["flux_vgrowth"]["xch"][reac]=0.
            #aff("ms for "+reac, ms);##

            # create chemical formula
            netan["formula"][reac]={"left":[(m, 1.)], "right":[(mgr, 1)], "all":[m,mgr]}
            # stoicheometry
            netan["sto_m_r"][m]["left"].append((reac, 1.))
            netan["sto_m_r"][mgr]={"left": [], "right": [(reac, 1.)]}
            netan["sto_r_m"][reac]={"left":[(m, 1.)], "right":[(mgr, 1.)]}

    # check metab names in pools and network
    # all met_pools must be in internal metabolites
    mdif=oset(netan["met_pools"]).difference(netan["metabint"])
    if len(mdif) :
        # unknown metabolite
        raise Exception("Unknown metabolite(s). Metabolite(s) '"+", ".join(mdif)+"' defined in the section METABOLITE_POOLS are not internal metabolites in the NETWORK section.")

    if netan["met_pools"] and me=="ftbl2labprop.py3":
        # all internal metabs must be also in met_pools section
        mdif=netan["metabint"].difference(netan["met_pools"])
        if len(mdif) :
            # unknown metabolite(s)
            raise Exception("Unknown metabolite(s). Metabolite(s) '"+", ".join(mdif)+"' defined as internal in the section NETWORK, are not defined in the METABOLITE_POOLS section.")
    
    # input and output flux names
    netan["flux_in"]=oset(f for m in netan["input"] for f,_ in netan["sto_m_r"][m]["left"])
    netan["flux_out"]=oset(f for m in netan["output"] for f,_ in netan["sto_m_r"][m]["right"])
    netan["flux_inout"]=netan["flux_in"] | netan["flux_out"]
    #print "fl in + out="+str(netan["flux_in"])+"+"+str( netan["flux_out"]);##
    
    # get possible additional fluxes (i.e. from cofactors) from EQAULITY section (eqflux)
    # flux equalities
    # list of tuples (value,dict) where dict is flux:coef
    # net fluxes
    
    # temporary list of constrained net fluxes
    fcnstr=oset(row["NAME"] for row in ftbl.get("FLUXES", dict()).get("NET", []) if row["FCD"] == "C")
    for row in ftbl.get("EQUALITIES", dict()).get("NET",[]):
        dicf=formula2dict(row["FORMULA"])
        # number of non constrained fluxes in a formula
        nb_nonc=sum(fl not in fcnstr for fl in dicf)
        if nb_nonc==0:
            wout("***Warning: in EQUALITIES/NET section, the formula '"+
                row["VALUE"]+"="+row["FORMULA"]+"' involves only constrained flux(es).\nThe equality is ignored as meaningless (%s: %s).\n"%(ftbl["name"], row["irow"]))
            continue
        netan["flux_equal"]["net"].append((
                eval_expr(row["VALUE"]),
                dicf,
                row["FORMULA"]+"="+row["VALUE"]+": "+str(row["irow"])))
    # xch fluxes
    # temporary list of constrained xch fluxes
    fcnstr=oset(row["NAME"] for row in ftbl.get("FLUXES", dict()).get("XCH", []) if row["FCD"] == "C")
    for row in ftbl.get("EQUALITIES", dict()).get("XCH",[]):
        dicf=formula2dict(row["FORMULA"])
        # number of non constrained fluxes in a formula
        nb_nonc=sum(fl not in fcnstr for fl in dicf)
        if nb_nonc==0:
            wout("***Warning: in EQUALITIES/XCH section, the formula '"+
                row["VALUE"]+"="+row["FORMULA"]+"' involves only constrained flux(es).\nThe equality is ignored as meaningless (%s: %s).\n"%(ftbl["name"], row["irow"]))
            continue
        netan["flux_equal"]["xch"].append((
                eval_expr(row["VALUE"]),
                dicf,
                row["FORMULA"]+"="+row["VALUE"]+": "+str(row["irow"])))
    netan["eqflux"]=oset(f for row in netan["flux_equal"]["net"] for f in [*row[1].keys()]) | oset(f for row in netan["flux_equal"]["xch"] for f in [*row[1].keys()])
    eqflux=netan["eqflux"]
    # metab EQAULITIES
    netan["metab_equal"]=list()
    for row in ftbl.get("EQUALITIES", dict()).get("METAB",[]):
        #print row;##
        dicf=formula2dict(row["FORMULA"])
        nb_neg=0
        for m in dicf:
            if m not in netan["metabint"]:
                raise Exception("Metabolite `%s` is not internal metabolite (%s: %s)."%(m, ftbl["name"], row["irow"]))
            if m not in netan["met_pools"]:
                raise Exception("Metabolite `%s` is not declared in METABOLITE_POOLS section (%s: %s)."%(m, ftbl["name"], row["irow"]))
            nb_neg+=netan["met_pools"][m]<0
        if nb_neg==0:
            raise Exception("At least one of metabolites '%s' must be declared as variable (i.e. having negative value) in the section METABOLITE_POOLS (%s: %s)."%("', '".join(list(dicf.keys())), ftbl["name"], row["irow"]))
        netan["metab_equal"].append((
                eval_expr(row["VALUE"]),
                dicf,
                row["FORMULA"]+"="+row["VALUE"]+": "+str(row["irow"])))

    # analyse reaction reversibility
    # free fluxes(dictionary flux->init value for simulation or minimization)
    # constrained fluxes (dictionary flux->value)
    # variable growth fluxes (dictionary flux->value)
    # The rest are dependent fluxes
    netan["flux_dep"]=dict({"net":dict(), "xch":dict()})
    netan["flux_free"]=dict({"net":dict(), "xch":dict()})
    netan["flux_constr"]=dict({"net":dict(), "xch":dict()})
    flx=ftbl.get("FLUXES")
    fcd=oset(("F", "C", "D"))
    if flx:
        xch=flx.get("XCH")
        net=flx.get("NET")
##        aff("net", net)
        # check that all fluxes are defined in network section
        allreac=netan["reac"] | netan["flux_inout"]
        unk=[ row["NAME"] for row in net if row["FCD"] in fcd and row["NAME"] not in allreac|eqflux ]
        if len(unk):
            raise Exception("The flux name(s) '%s' from the FLUX/NET section is (are) not defined in the NETWORK neither EQUALITY section."%(", ".join(unk)))
        unk=[ row["NAME"] for row in xch if row["FCD"] in fcd and row["NAME"] not in allreac|eqflux ]
        if len(unk):
            raise Exception("The flux name(s) '%s' from the FLUX/XCH section is (are) not defined in the NETWORK neither EQUALITY section."%(", ".join(unk)))

        # check that all reactions from NETWORK are at least in FLUX/NET section
        fnet=oset( row["NAME"] for row in net if row["NAME"] and row["FCD"] in fcd )
        #print("fnet=", fnet)
        #print("allreac=", allreac)
        unk=allreac-fnet-oset(netan["flux_growth"]["net"])-oset(netan["flux_vgrowth"]["net"])
        if len(unk):
            raise Exception("The flux name(s) '%s' from the NETWORK section is (are) not defined in the FLUX/NET section."%(", ".join(unk)))

        # check that all reactions from EQUALITY are at least in FLUX/NET/XCH section
        fnetxch=oset( row["NAME"] for row in net+xch if row["NAME"] and row["FCD"] in fcd )
        unk = eqflux - fnet - oset(netan["flux_growth"]["net"]) - oset(netan["flux_vgrowth"]["net"])
        if len(unk):
            raise Exception("The flux name(s) '%s' from the NETWORK section is (are) not defined in the FLUX/NET section."%(", ".join(unk)))
    else:
        werr(os.path.basename(me)+": netan[FLUXES] is not defined\n")
        #return None
    #print "list reac=", netan["reac"];##
    try:
        #print( netan["reac"] | netan["flux_inout"])
        for reac in netan["reac"] | netan["flux_inout"] | eqflux:
            #print("reac=", reac)
            # get xch condition for this reac
            cond=[row for row in xch if row["NAME"]==reac]
            # get net condition for this reac
            ncond=([row for row in net if row["NAME"]==reac]) if net else []
            # no xch dispatch check for input/output fluxes as they are
            # constrained by definition
            #print "r,c,n=", reac, len(cond), len(ncond);##
            if len(cond) > 1:
                raise ValueError("FluxDef", "Reaction `%s` is over defined in exchange fluxes."%reac)
            if len(cond) == 0 and not (reac in (netan["flux_in"] | netan["flux_out"])):
                werr("in+out fluxes="+str(netan["flux_inout"])+"\n");##
                raise ValueError("FluxDef", "Reaction `%s` is not defined D/F/C in exchange fluxes."%reac)
            if cond:
                cond=cond[0]
            if len(ncond) > 1:
                raise ValueError("FluxDef", "Reaction `%s` is over defined in net fluxes."%reac)
            if len(ncond) == 0 and not (reac in (netan["flux_inout"])):
                raise ValueError("FluxDef", "Reaction `%s` is not defined D/F/C in net fluxes."%reac)
            if (len(ncond) == 0 and
                (reac in netan["flux_inout"] and
                not reac in netan["flux_growth"]["net"] and
                not reac in netan["flux_vgrowth"]["net"])):
                # input-output fluxes are by definition not reversible
                netan["notrev"].add(reac)
                netan["flux_constr"]["xch"][reac]=0.
                continue
            if netan["opt"].get("include_growth_flux") and reac in netan["flux_growth"]["net"]:
                m=reac[:-3]
                netan["notrev"].add(reac)
                netan["flux_constr"]["xch"][reac]=0.
                netan["flux_constr"]["net"][reac]=netan["opt"]["mu"]*netan["met_pools"][m]
                continue
            if netan["opt"].get("include_growth_flux") and reac in netan["flux_vgrowth"]["net"]:
                m=reac[:-3]
                netan["notrev"].add(reac)
                netan["flux_vgrowth"]["net"][reac]=netan["opt"]["mu"]*abs(netan["met_pools"][m])
                netan["flux_vgrowth"]["xch"][reac]=0.
                continue
            ncond=ncond[0]
            #aff("cond", cond);##
            #aff("ncond", ncond);##
            # not reversibles are those reaction having xch flux=0 or
            # input/output fluxes
            val=eval_expr(cond["VALUE(F/C)"])
            xval=0. if val is None else float(val)
            val=eval_expr(ncond["VALUE(F/C)"])
            nval=0. if val is None else float(val)
            try:
                if (cond and cond["FCD"] == "C") or (reac in netan["flux_inout"]):
                    if (reac in netan["flux_inout"]) or \
                        (xval == 0.):
                        netan["notrev"].add(reac)
                        netan["flux_constr"]["xch"][reac]=0.
                    else:
                        # not null constrained xch
                        if xval < 0. or xval > 1.:
                            raise ValueError("FluxVal", "Constrained exchange flux`%s` must have value in [0; 1] interval.\nInstead, a value %f is given"%(reac, xval))
                        netan["flux_constr"]["xch"][reac]=xval
                elif (cond and cond["FCD"] == "F"):
                    # free xch
                    if xval < 0. or xval > 1.:
                        raise ValueError("FluxVal", "Free exchange flux '%s' must have starting value in [0; 1] interval.\nInstead, a value %f is given"%(reac, xval))
                    netan["flux_free"]["xch"][reac]=xval
                elif (cond and cond["FCD"] == "D"):
                    # dependent xch
                    netan["flux_dep"]["xch"][reac]=xval
                if (ncond and ncond["FCD"] == "F"):
                    # free net
                    netan["flux_free"]["net"][reac]=nval
                elif (ncond and ncond["FCD"] == "C"):
                    # constr net
                    netan["flux_constr"]["net"][reac]=nval
                elif (ncond and ncond["FCD"] == "D"):
                    # dependent net
                    netan["flux_dep"]["net"][reac]=nval
            except ValueError:
                werr("Error occured on the row %s or %s of ftbl file %s:\n"%(ncond["irow"], cond["irow"], ftbl["name"]))
                raise
    except ValueError as inst:
        #werr(join(" ", sys.exc_info())+"\n")
        #werr(": ".join(inst)+"\n")
        werr("\n"+format(inst)+"\n")
    # complete variable growth exchange fluxes
    netan["flux_vgrowth"]["xch"]=dict((k, 0.) for k in netan["flux_vgrowth"]["net"])
    
    # measured fluxes
    for row in ftbl.get("FLUX_MEASUREMENTS",[]):
        if row["FLUX_NAME"] not in netan["reac"]|eqflux:
            raise Exception("Mesured flux `%s` is not defined in NETWORK section neither in EQUALITIES (%s: %s)."%(row["FLUX_NAME"], ftbl["name"], row["irow"]))
        if row["FLUX_NAME"] not in netan["flux_free"]["net"] and \
            row["FLUX_NAME"] not in netan["flux_dep"]["net"]:
            raise Exception("Mesured flux `%s` must be defined as either free or dependent (%s: %s)."%(row["FLUX_NAME"], ftbl["name"], row["irow"]))
        val=eval_expr(row["VALUE"])
        fval=NaN if val is None else float(val)
        val=eval_expr(row["DEVIATION"])
        sdev=None if val is None else float(val)
        if sdev is None:
            raise Exception("DEVIATION (%s) must evaluate to a real number (%s: %s)."%(row["DEVIATION"], ftbl["name"], row["irow"]))
        if sdev <= 0.:
            raise Exception("DEVIATION (%s) must be positive (%s: %s)."%(row["DEVIATION"], ftbl["name"], row["irow"]))
        
        netan["flux_measured"][row["FLUX_NAME"]]={\
                "val": fval, \
                "dev": sdev}
    # measured concentartions
    for row in ftbl.get("METAB_MEASUREMENTS",[]):
        metabl=row["META_NAME"].split("+")
        found_neg=False
        for m in metabl:
            if m not in netan["metabint"]:
                raise Exception("Mesured metabolite `%s` is not internal metabolite (%s: %s)."%(m, ftbl["name"], row["irow"]))
            if m not in netan["met_pools"]:
                raise Exception("Mesured metabolite `%s` is not declared in METABOLITE_POOLS section (%s: %s)."%(m, ftbl["name"], row["irow"]))
            found_neg=found_neg or netan["met_pools"][m] < 0.
        if not found_neg:
            werr("Warning: metabolite measurements on `%s` does not have a free metabolite (i.e. being negative in the METABOLITE_POOLS ((%s: %s)).\n"%(row["META_NAME"], ftbl["name"], row["irow"]))
            werr("This measurement is ignored\n")
            continue
        try:
            val=float(eval_expr(row["VALUE"]))
        except ValueError:
            val=NaN
        try:
            sdev=float(eval_expr(row["DEVIATION"]))
        except ValueError:
            raise ValueError("DEVIATION must evaluate to a real positive number (%s: %s)."%(ftbl["name"], row["irow"]))
        if sdev <= 0.:
            raise ValueError("DEVIATION must be positive (%s: %s)."%(ftbl["name"], row["irow"]))
        netan["metab_measured"][row["META_NAME"]]={\
                "val": val, \
                "dev": sdev}
    
    # proceed LABEL_INPUT
    proc_label_input(ftbl, netan, case_i)
    # flux inequalities
    # list of tuples (value,comp,dict) where dict is flux:coef
    # and comp is of "<", "<=", ...
    # net fluxes
    for row in ftbl.get("INEQUALITIES", dict()).get("NET",[]):
        #print row;##
        if row["COMP"] not in (">=", "=>", "<=", "=<"):
            raise Exception("COMP field in INEQUALITIES section must be one of '>=', '=>', '<=', '=<' and not '%s' (%s: %s)."%(row["COMP"], ftbl["name"], row["irow"]))
        dicf=formula2dict(row["FORMULA"])
        fl=list(dicf.keys())[0]
        if len(dicf)==1 and fl in netan["flux_constr"]["net"]:
            wout("***Warning: Inequalities: in NET section, the formula '"+
                row["VALUE"]+row["COMP"]+row["FORMULA"]+"' involves a constrained flux\n"+
                " having a value "+str(netan["flux_constr"]["net"][fl])+". The inequality is ignored as meaningless ((%s: %s)).\n"%(ftbl["name"], row["irow"]))
            continue
        val=eval_expr(row["VALUE"])
        if val is None:
            raise Exception("VALUE field in INEQUALITIES section must evaluate to a float number '%s' (%s: %s)."%(row["VALUE"], ftbl["name"], row["irow"]))
        netan["flux_inequal"]["net"].append((
                float(val),
                row["COMP"],
                dicf))
    # xch fluxes
    for row in ftbl.get("INEQUALITIES", dict()).get("XCH",[]):
        #print row;##
        if row["COMP"] not in (">=", "=>", "<=", "=<"):
            raise Exception("COMP field in INEQUALITIES section must be one of '>=', '=>', '<=', '=<' and not '%s' (%s: %s)."%(row["COMP"], ftbl["name"], row["irow"]))
        val=eval_expr(row["VALUE"])
        if val is None:
            raise Exception("VALUE field in INEQUALITIES section must evaluate to a float number '%s' (%s: %s)."%(row["VALUE"], ftbl["name"], row["irow"]))
        netan["flux_inequal"]["xch"].append((
                float(val),
                row["COMP"],
                formula2dict(row["FORMULA"])))
    for (afftype, ftype) in (("Net", "net"), ("Exchange", "xch")):
        for row in netan["flux_inequal"][ftype]:
            for fl in row[2]:
                if fl not in netan["reac"]|eqflux:
                    raise Exception("%s flux `%s` in the inequality\n%s\nis not defined in NETWORK neither EQUALITY sections."%
                        (afftype, fl, join("", row)))
    # metabolite inequalities (like the flux ones)
    netan["metab_inequal"]=list()
    for row in ftbl.get("INEQUALITIES", dict()).get("METAB",[]):
        dicf=formula2dict(row["FORMULA"])
        if row["COMP"] not in (">=", "=>", "<=", "=<"):
            raise Exception("COMP field in INEQUALITIES section must be one of '>=', '=>', '<=', '=<' and not '%s' (%s: %s)."%(row["COMP"], ftbl["name"], row["irow"]))
        nb_neg=0
        for m in dicf:
            if m not in netan["metabint"]:
                raise Exception("Metabolite `%s` is not internal metabolite (%s: %s)."%(m, ftbl["name"], row["irow"]))
            if m not in netan["met_pools"]:
                raise Exception("Metabolite `%s` is not declared in METABOLITE_POOLS section (%s: %s)."%(m, ftbl["name"], row["irow"]))
            nb_neg+=netan["met_pools"][m]<0
        if nb_neg==0:
            raise Exception("At least one of metabolites '%s' must be declared as variable (i.e. having negative value) in the section METABOLITE_POOLS (%s: %s)."%("', '".join(list(dicf.keys())), ftbl["name"], row["irow"]))
        val=eval_expr(row["VALUE"])
        if val is None:
            raise Exception("VALUE content '%s' must evaluate to a float number, cf. the section METABOLITE_POOLS (%s: %s)."%(row["VALUE"], ftbl["name"], row["irow"]))
        netan["metab_inequal"].append((
                float(val),
                row["COMP"],
                dicf,
                row["VALUE"]+row["COMP"]+row["FORMULA"]+": "+str(row["irow"])))

    # Check that fluxes are all in reactions and eqflux
    # then form nx2dfcg dictionary
    for (affnx, nx, nxsh) in (("net", "net", "n."), ("exchange", "xch", "x.")):
        for (affdfcg, dfcg, dfcgsh) in (("Dependent", "flux_dep", "d."), ("Free", "flux_free", "f."), ("Constrained", "flux_constr", "c."), ("Variable growth", "flux_vgrowth", "g.")):
            #print netan[dfcg][nx];##
            for fl in netan[dfcg][nx]:
                if fl not in netan["reac"]|eqflux:
                    raise Exception("%s %s flux `%s` is not defined in NETWORK neither EQUALITY sections."%
                       (affdfcg, affnx, fl))
                netan["nx2dfcg"][nxsh+fl]=dfcgsh+nxsh+fl
    # labeled measurements
    proc_label_meas(ftbl, netan)
    proc_peak_meas(ftbl, netan)
    proc_mass_meas(ftbl, netan)
    
    # discard empty entries
    for e in netan:
        try:
            netan[e].discard("")
            netan[e].discard(None)
        except AttributeError: pass

    # fwd-rev flux balance
    del(res)
    res=netan["flux_m_r"]
    # res[metab]["in"]=list(fwd.flux|rev.flux)
    # res[metab]["out"]=list(fwd.flux|rev.flux)
    for metab,lr in netan["sto_m_r"].items():
        # lr is dico with 'left' and 'right' entries
        #print "fwd metab="+str(metab);##
        if not metab in res:
            res[metab]={"in":[], "out":[]}
        for reac,_ in lr["left"]:
            # here metabolite is consumed in fwd reaction
            # and produced in the reverse one.
            res[metab]["out"].append("fwd."+reac)
            #if reac not in netan["notrev"]:
            if reac not in netan["flux_inout"]:
                res[metab]["in"].append("rev."+reac)
        for reac,_ in lr["right"]:
            # here metabolite is consumed in rev reaction
            # and produced in the forward one.
            res[metab]["in"].append("fwd."+reac)
            #if reac not in netan["notrev"]:
            if reac not in netan["flux_inout"]:
                res[metab]["out"].append("rev."+reac)

    # metabolite network
    res=netan["metab_netw"]
    # left part or reaction points to right part
    for reac,parts in netan["formula"].items():
        for metab,_ in parts["left"]:
            if not metab in res:
                res[metab]=oset()
            res[metab].update(m for m,_ in parts["right"])
    # order cumomers by weight. For a given weight, cumomers are sorted by
    # metabolites order.
    netan["Cmax"]=max(netan["Clen"].values())
    Cmax=netan["Cmax"]
    #return netan;##
    # cumomers systems A*x=b, one by weight
    # weights are going from 1 to Cmax where Cmax is the maximal
    # carbon string length in all metabolites
    # A is a list of matrices (in weight order)
    # b is a list of right hand parts (still in weight order)
    # the dimensions of various weights in b are not the same
    # too short metabolites are dropped when going to higher weights.
    res=netan["cumo_sys"]
    res["A"]=[{} for i in range(Cmax)]
    res["b"]=[{} for i in range(Cmax)]
    try:
        # run through all reactions and update bilan of involved cumomers
        for (reac,lrdict) in iter(netan["carbotrans"].items()) if fullsys else []:
            # run through metabs
            ## aff("lrdict", lrdict);#
            for (imetab,lr,metab,cstr) in ((imetab,lr,metab,cstr)
                    for (lr,lst) in lrdict.items()
                    for (imetab,(metab,cstr)) in enumerate(lst)):
                # if output metab then influx is set to 1
                # so its cumomer distribution is directly
                # defined by cumodistr of inputs
                Clen=netan["Clen"][metab]
                # input metabolite has fixed value so put it in rhs
                # when it is an influx for some internal cumomer
                if metab in netan["input"] or metab in netan["output"]:
                    continue
                # 'out' part of this metab
                fwd_rev=("fwd." if lr=="left" else "rev.")
                flux=fwd_rev+reac
                #if (fwd_rev=="fwd." or reac not in netan["notrev"]):
                if (fwd_rev=="fwd." or reac not in netan["flux_inout"]):
                    # add this out-flux
                    # run through all cumomers of metab
                    for icumo in range(1,1<<Clen):
                        cumo=metab+":"+str(icumo)
                        w=sumbit(icumo)
                        #print "w,i,clen,metab=", w, icumo,Clen,metab;##
                        if cumo not in res["A"][w-1]:
                            res["A"][w-1][cumo]={cumo:[]}
                        # main diagonal term ('out' part)
                        res["A"][w-1][cumo][cumo].append(flux)
                        ##print 'm,ic,w='+metab, icumo, w;#
                        ##aff("res["A"][w-1][cumo][cumo]", res["A"][w-1][cumo][cumo]);#
                # 'in' part
                fwd_rev=("rev." if lr=="left" else "fwd.")
                flux=fwd_rev+reac
                in_lr=("left" if lr=="right" else "right")
                #if (fwd_rev=="rev." and reac in netan["notrev"]):
                if (fwd_rev=="rev." and reac in netan["flux_inout"]):
                    # this cannot be by definition
                    continue
                # add this in-flux
                for (in_i,(in_metab, in_cstr)) in enumerate(lrdict[in_lr]):
                    # run through all cumomers of metab
                    for icumo in range(1,1<<Clen):
                        cumo=metab+":"+str(icumo)
                        w=sumbit(icumo)
                        # get in_cumo
                        in_icumo=src_ind(in_cstr, cstr, icumo)
                        if in_icumo==None:
                            continue
                        in_cumo=in_metab+":"+str(in_icumo)
                        in_w=sumbit(in_icumo)
                        if cumo not in res["A"][w-1]:
                            res["A"][w-1][cumo]={cumo:[]}
                        if in_w==w:
                            if in_metab in netan["input"]:
                                # put it in rhs
                                if cumo not in res["b"][w-1]:
                                    res["b"][w-1][cumo]=dict()
                                if flux not in res["b"][w-1][cumo]:
                                    res["b"][w-1][cumo][flux]=dict()
                                if imetab not in res["b"][w-1][cumo][flux]:
                                    res["b"][w-1][cumo][flux][imetab]=[]
                                if not netan["cumo_input"] or in_cumo not in netan["cumo_input"][0]:
                                    # put this in_cumo ih the dict
                                    iso2cumo(netan, "cumo_input", in_cumo, in_icumo, in_metab)
                                res["b"][w-1][cumo][flux][imetab].append(in_cumo)
                            else:
                                if in_cumo not in res["A"][w-1][cumo]:
                                    res["A"][w-1][cumo][in_cumo]=[]
                                # matrix: linearized off-diagonal term
                                res["A"][w-1][cumo][in_cumo].append(flux)
                        elif in_w < w:
                            # put lighter cumomer product in rhs list[iterm]
                            if cumo not in res["b"][w-1]:
                                res["b"][w-1][cumo]=dict()
                            if flux not in res["b"][w-1][cumo]:
                                res["b"][w-1][cumo][flux]=dict()
                            if imetab not in res["b"][w-1][cumo][flux]:
                                res["b"][w-1][cumo][flux][imetab]=[]
                            #res["b"][w-1][cumo][flux][imetab].append(
                            #    in_cumo if in_metab not in netan["input"] else
                            #    netan["cumo_input"][in_cumo])
                            res["b"][w-1][cumo][flux][imetab].append(in_cumo)
                            #print "b="+str(res["b"][w-1][cumo][flux]);##
                        # if in_w==0 then in_cumo=1 by definition => ignore here
                        # in_w cannot be > w because of src_ind()
    except Exception as inst:
        werr(": ".join(inst)+"\n")
    #netan["cumo_input"]=[dict((k,(v if v==v else 0.)) for k,v in d.items()) for d in netan["cumo_input"]]
    # ordered cumomer lists
    #for w in range(1,netan["Cmax"]+1):
        # weight 1 equations have all metabolites
        ##aff("A "+str(w), netan["cumo_sys"]["A"][w-1]);#
        ##aff("b "+str(w), netan["cumo_sys"]["b"][w-1]);#
        # order cumos along pathways
        # starts are input cumomers
    #    starts=[cumo for cumo in netan["cumo_sys"]["A"][w-1] \
    #        if cumo.split(":")[0] in netan["input"]]
        ##aff("st "+str(w), starts)
        # complete starts by all others cumomers
    #    starts+=[c for c in netan["cumo_sys"]["A"][w-1] if not c in starts]
    #    cumo_paths=cumo_path(starts, netan["cumo_sys"]["A"][w-1], oset())
        # order
    #    netan["vcumo"].append([cumo for cumo in valval(cumo_paths)])
    netan["vcumo"]=[[*a.keys()] for a in netan["cumo_sys"]["A"]]

    # ordered unknown flux lists
    # get all reactions which are not constrained, not free and not growth
    netan["vflux"]["net"].extend(reac for reac in netan["reac"]|eqflux
        if reac not in netan["flux_constr"]["net"] and
        reac not in netan["flux_free"]["net"] and
        reac not in netan["flux_vgrowth"]["net"])
    netan["vflux"]["xch"].extend(reac for reac in netan["reac"]|eqflux
        if reac not in netan["flux_constr"]["xch"] and
        reac not in netan["flux_free"]["xch"] and
        reac not in netan["flux_vgrowth"]["xch"])

    # order
    netan["vflux"]["net"].sort()
    netan["vflux"]["xch"].sort()
    
    # easy index finder
    netan["vflux"]["net2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux"]["net"]))
    netan["vflux"]["xch2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux"]["xch"]))


    # ordered free flux lists
    netan["vflux_free"]["net"]=list(netan["flux_free"]["net"].keys())
    netan["vflux_free"]["xch"]=list(netan["flux_free"]["xch"].keys())

    # order
    netan["vflux_free"]["net"].sort()
    netan["vflux_free"]["xch"].sort()
    
    # easy index finder
    netan["vflux_free"]["net2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux_free"]["net"]))
    netan["vflux_free"]["xch2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux_free"]["xch"]))


    # ordered constrained flux lists
    netan["vflux_constr"]["net"]=list(netan["flux_constr"]["net"].keys())
    netan["vflux_constr"]["xch"]=list(netan["flux_constr"]["xch"].keys())

    # order
    netan["vflux_constr"]["net"].sort()
    netan["vflux_constr"]["xch"].sort()
    
    # easy index finder
    netan["vflux_constr"]["net2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux_constr"]["net"]))
    netan["vflux_constr"]["xch2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux_constr"]["xch"]))


    # ordered measured flux lists
    netan["vflux_meas"]["net"]=list(netan["flux_measured"].keys())

    # order
    netan["vflux_meas"]["net"].sort()
    
    # easy index finder
    netan["vflux_meas"]["net2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux_meas"]["net"]))

    # ordered variable growth fluxes
    netan["vflux_growth"]["net"]=list(netan["flux_vgrowth"]["net"].keys())
    netan["vflux_growth"]["xch"]=list(netan["flux_vgrowth"]["xch"].keys())

    # order
    netan["vflux_growth"]["net"].sort()
    netan["vflux_growth"]["xch"].sort()
    
    # easy index finder
    netan["vflux_growth"]["net2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux_growth"]["net"]))
    netan["vflux_growth"]["xch2i"]=dict((fl,i) for (i,fl) in enumerate(netan["vflux_growth"]["xch"]))

    # ordered complete flux lists
    netan["vflux_compl"]={
        "net": 
        netan["vflux"]["net"]+
        netan["vflux_free"]["net"]+
        netan["vflux_constr"]["net"]+
        netan["vflux_growth"]["net"],
        "xch":
        netan["vflux"]["xch"]+
        netan["vflux_free"]["xch"]+
        netan["vflux_constr"]["xch"]+
        netan["vflux_growth"]["xch"],
    }
    # easy index finding
    # net
    netan["vflux_compl"]["net2i"]=dict(
        (fl,i) for (i,fl) in enumerate(netan["vflux"]["net"])
    )
    netan["vflux_compl"]["net2i"].update(dict(
        (fl,i+len(netan["vflux"]["net"]))
        for (i,fl) in enumerate(netan["vflux_free"]["net"])
    ))
    netan["vflux_compl"]["net2i"].update(dict(
        (fl,i+len(netan["vflux"]["net"])+len(netan["vflux_free"]["net"]))
        for (i,fl) in enumerate(netan["vflux_constr"]["net"])
    ))
    netan["vflux_compl"]["net2i"].update(dict(
        (fl,i+len(netan["vflux"]["net"])+len(netan["vflux_free"]["net"])+len(netan["vflux_constr"]["net"]))
        for (i,fl) in enumerate(netan["vflux_growth"]["net"])
    ))
    # xch
    netan["vflux_compl"]["xch2i"]=dict(
        (fl,i) for (i,fl) in enumerate(netan["vflux"]["xch"])
    )
    netan["vflux_compl"]["xch2i"].update(dict(
        (fl,i+len(netan["vflux"]["xch"]))
        for (i,fl) in enumerate(netan["vflux_free"]["xch"])
    ))
    netan["vflux_compl"]["xch2i"].update(dict(
        (fl,i+len(netan["vflux"]["xch"])+len(netan["vflux_free"]["xch"]))
        for (i,fl) in enumerate(netan["vflux_constr"]["xch"])
    ))
    netan["vflux_compl"]["xch2i"].update(dict(
        (fl,i+len(netan["vflux"]["xch"])+len(netan["vflux_free"]["xch"])+len(netan["vflux_constr"]["xch"]))
        for (i,fl) in enumerate(netan["vflux_growth"]["xch"])
    ))

    # ordered fwd-rev flux lists
    # fw and rv parts are identical so they match each other
    netan["vflux_fwrv"]["fwrv"]=\
        ["fwd."+fl for fl in netan["vflux"]["net"]]+\
        ["fwd."+fl for fl in netan["vflux_free"]["net"]]+\
        ["fwd."+fl for fl in netan["vflux_constr"]["net"]]+\
        ["fwd."+fl for fl in netan["vflux_growth"]["net"]]+\
        ["rev."+fl for fl in netan["vflux"]["net"]]+\
        ["rev."+fl for fl in netan["vflux_free"]["net"]]+\
        ["rev."+fl for fl in netan["vflux_constr"]["net"]]+\
        ["rev."+fl for fl in netan["vflux_growth"]["xch"]]
    
    # order
    netan["vflux_fwrv"]["fwrv"].sort()
    
    # easy index finder
    netan["vflux_fwrv"]["fwrv2i"]=dict((fl,i) for (i,fl) in
        enumerate(netan["vflux_fwrv"]["fwrv"]))
        
    # ordered metabolite pools
    netan["vpool"]={
        "free": [m for (m,v) in netan["met_pools"].items() if v < 0.],
        "constrained": [m for (m,v) in netan["met_pools"].items() if v >= 0.],
    }
    netan["vpool"]["free"].sort()
    netan["vpool"]["constrained"].sort()
    netan["vpool"]["all"]=netan["vpool"]["free"]+netan["vpool"]["constrained"]
    # easy index finding
    netan["vpool"]["all2i"]=dict((m,i) for (i,m) in
        enumerate(netan["vpool"]["all"]))
    
    
    # linear problem on fluxes Afl*(fl_net;fl_xch)=bfl
    # matrix Afl is composed of the following parts :
    # - stocheometic equations (only .net fluxes are involved)
    # - flux equalities
    # constrained to non zero value fluxes are replaced by their values in rhs
    # Afl is a list of lists (two dim array). Primary list is a matrix row,
    # columns are values in the secondary list and contains matrix coefficients
    # bfl is a list of linear expressions. Each expression is a dict
    # where keys are variable names like "f.n.flx" and values are
    # numeric coefficients
    # If the key is empty "", it means that the value is just a scalar to add
    
    # Full flux names are of the format [dfcg].[nx].<reac>
    # where "d", "f", "c" or "g" correspond to
    # - dependent
    # - free
    # - constrained
    # - growth
    # and <reac> correspond to the reaction name
    
    # stocheometric part
    res=netan["Afl"]
    for (metab,lr) in netan["sto_m_r"].items():
        #if metab == "M_adp_c":
            #pdb.set_trace()
        if metab in netan["input"] or metab in netan["output"]:
            continue
        # calculate coefs (repeated fluxes)
        coefs=dict((rea, []) for rea,co in lr["left"]+lr["right"])
        # 'left' part consumes metab, hence the sign '-' in -co
        _=[coefs[rea].append(-co) for rea,co in lr["left"]]
        # 'right' part produces metab
        _=[coefs[rea].append(co) for rea,co in lr["right"]]
        coefs=dict((rea, sum(li)) for rea,li in coefs.items())
        deps=oset(coefs.keys()).intersection(netan["vflux"]["net"])
        if not deps:
            raise Exception("A balance on metabolite '%s' does not contain any dependent flux.\nAt least one of the following net fluxes %s\nmust be declared dependent in the FLUX/NET section (put letter 'D' in the column 'FCD' for some flux)."%(metab, list(coefs.keys())))
        qry=[coefs.get(fl,0) for fl in netan["vflux"]["net"]]
        qry.extend([0]*len(netan["vflux"]["xch"]))
        mqry=-np.array(qry)
        #if qry==[0]*len(qry): must be included even if all zeros, so an R warning will work
        #    # degenerated equation, skip it
        #    #netan["flux_equal"]["net"].append((0., coefs))
        #    raise Exception("Stocheometric equation is zero for metab "+metab+"\n"+str(lr)+"\n"+str(coefs))
        #    continue
        # check if this line was already entered before
        for (i,row) in enumerate(res):
            if not ffguess and (row == qry or (np.array(row) == mqry).all()):
                inz=set(i for i,coef in enumerate(row) if coef !=0)
                wout("***Warning: when trying to add a balance equation for metabolite '"+metab+
                    "', got equation redundant with those for '"+netan["vrowAfl"][i]+"'\n")
                wout("metab:\t"+join("\t", [fl for i,fl in enumerate(netan["vflux"]["net"]+netan["vflux"]["xch"]) if i in inz])+"\n")
                wout(netan["vrowAfl"][i]+":\t"+join("\t", [coef for i,coef in enumerate(row) if i in inz])+"\n")
                wout(metab+":\t"+join("\t", [coef for i,coef in enumerate(qry) if i in inz])+"\n")
                break
        else:
            # identique row is not found, add it
            res.append(qry)
            netan["vrowAfl"].append(metab)
            # prepare right hand side
            netan["bfl"].append({})
            dtmp=netan["bfl"][-1]
            for fl in coefs:
                #print "bfl: fl="+fl;##
                if fl in netan["flux_free"]["net"]:
                    dtmp["f.n."+fl]=dtmp.get("f.n."+fl,0)-coefs[fl]
                elif fl in netan["flux_constr"]["net"]:
                    dtmp["c.n."+fl]=dtmp.get("c.n."+fl,0)-coefs[fl]
                elif fl in netan["flux_vgrowth"]["net"]:
                    dtmp["g.n."+fl]=dtmp.get("g.n."+fl,0)-coefs[fl]
            #print "dtmp=", dtmp

    # flux equality part
    res=netan["Afl"]
    for (nx, nxl) in (("net", "n"), ("xch", "x")):
        for eq in netan["flux_equal"][nx]:
            qry=[]
            qry.extend(eq[1].get(fl,0) for fl in netan["vflux"][nx])
            if nx == "net":
                # add xch zeros
                qry.extend([0]*len(netan["vflux"]["xch"]))
            else:
                # prepend zeros for net fluxes
                qry[0:0]=[0]*len(netan["vflux"]["net"])
            # check qry
            if qry == [0]*len(qry):
                # degenerated equality
                raise Exception("Equality in "+nx.upper()+" section: "+str(eq)+" must have at least one dependent flux\n")
            # check if this line was already entered before
            if not ffguess:
                mqry=-np.array(qry)
                for row in res:
                    if row == qry or (np.array(row) == mqry).all():
                        raise Exception("An equality in "+nx.upper()+" section is redundant. eq:"+str(eq)+
                            "\nqry="+str(qry)+"\nrow="+str(row))
            res.append(qry)
            netan["vrowAfl"].append("eq "+nx+": "+eq[2])
            netan["bfl"].append({"":eq[0]})
            dtmp=netan["bfl"][-1]
            # pass free fluxes to rhs
            for fl in netan["flux_free"][nx]:
                if fl in eq[1]:
                    dtmp["f."+nxl+"."+fl]=dtmp.get("f."+nxl+"."+fl,0)-float(eq[1][fl])
            # pass constrained fluxes to rhs
            for fl in netan["flux_constr"][nx]:
                if fl in eq[1]:
                    dtmp["c."+nxl+"."+fl]=dtmp.get("c."+nxl+"."+fl,0)-float(eq[1][fl])
            # pass growth fluxes to rhs
            for fl in netan["flux_vgrowth"][nx]:
                if fl in eq[1]:
                    dtmp["g."+nxl+"."+fl]=dtmp.get("c."+nxl+"."+fl,0)-float(eq[1][fl])

    # read parallel experiments if any
    proc_kinopt(ftbl, netan)
    if "opt" in netan and "prl_exp" in netan["opt"] and netan["opt"]["prl_exp"]:
        # parse ftbl files
        fli=re.split(r"\s*;\s*", netan["opt"]["prl_exp"])
        dirw=os.path.dirname(ftbl["abs_path"])
        for fn in fli:
            fn=fn.strip()
            if not fn:
                continue
            fp=ftbl_parse(os.path.join(dirw, fn))
            # test the presence of at least one required field
            if not any(f in fp for f in req_prl):
                raise Exception("Not found label input and/or measurements in '%s'"%fn)
            netan["exp_names"].append(fp["base_name"])
            proc_label_input(fp, netan, case_i)
            proc_label_meas(fp, netan)
            proc_peak_meas(fp, netan)
            proc_mass_meas(fp, netan)
            proc_kinopt(fp, netan)

def enum_path(starts, netw, outs, visited=oset()):
    """Enumerate metabilites along to reaction pathways.
    Algo: start from an input, follow chemical pathways till an output or
    already visited metabolite. Returns a list of metabolite pathways.
    Each pathways is an ordered list."""
    res=[]
    if (len(visited) == len(list(netw.keys()))):
        # no more metabolites 
        return res
    for start in starts:
        if (start in visited):
           continue
        visited.add(start)
        if start in outs:
            return [[start]]
        paths=enum_path(netw[start]-visited, netw, outs, visited)
        if (paths):
            res.append([start]+paths[0])
            if len(paths) > 1:
                res.extend(paths[1:])
        else:
            res.append([start])
    return res
def cumo_path(starts, A, visited=oset()):
    """Enumerate cumomers along reaction pathways.
    Algo: start from an input, follow chemical pathways till no more
    neighbours or till only visited metabolite rest in network.
    Return a list of cumomer pathways.
    Each pathways is an ordered list."""
    #if [s for s in starts if s.find('.') >= 0]:
    #    aff('s', starts)
    res=[]
    if (len(visited) == len(A)):
        # no more cumomers
        return res
    for start in starts:
        if (start in visited or not start in A):
           continue
        visited.add(start)
        # next_starts are cumos influenced by start
        next_starts=oset(cumo for cumo in A if start in A[cumo])
##        aff('next to '+start, next_starts)
        next_starts-=visited
        if next_starts:
            paths=cumo_path(next_starts, A, visited)
##            aff('paths for '+start, paths)
##            aff('visited', visited)
        else:
            path=[]
            res.append([start])
            continue
        if (paths):
            res.append([start]+paths[0])
            if len(paths) > 1:
                res.extend(paths[1:])
        else:
            res.append([start])
##    aff('r', res)
    return res
    
def src_ind(substrate, product, iprod):
    """
    For a given substrate and product carbon strings (e.g. "abc", "ab")
    calculate substrate index corresponding to product index.
    Return None if no source found.
    Return 0 if iprod==0 and intersection of product and substrate strings
    is not empty"""
    movbit=1
    isubstr=0
    substrate=substrate[::-1]
    product=product[::-1]
    for nb in range(len(product)):
        if (movbit & iprod):
            # algo: if the current bit is set in product search for its origin
            # and set the corresponding bit in substrate
            try:
                isubstr|=1<<substrate.find(product[nb])
            except ValueError:
                pass
        movbit<<=1
    return isubstr if (isubstr or
        (iprod==0 and (oset(product) & oset(substrate)))) else None
def labprods(prods, metab, isostr, strs):
    """labprods(prods, metab, isostr, strs)
    Return a set of tuples (vmetab,visostr) which receive at least
    one labeled carbon from (metab, isostr)
    """
    # run through product part of reaction to get
    # metabs containing labeled letter from isostr
    #print "p=", prods, "m=", metab, "i=", isostr, "s=", strs;##
    res=oset()
    # get labeled letters
    lets="".join(s[i] for (i,carb) in enumerate(isostr) if carb=="1" for s in strs)
    #print "ls=", lets;##
    # find labeled letters in reaction products and set
    # them to "1", while others to "0"
    for (vm,vs) in prods:
        rs=""
        for r in vs:
            rs=rs+("1" if r in lets else "0")
            #print "r=",r, "rs=",rs
        if rs.find("1") > -1:
            res.add((vm,rs))
            #print "append=", vm, rs;##
    #print "res=", res;##
    return res
def allprods(srcs, prods, isos, metab, isostr):
    """allprods(srcs, prods, isos, metab, isostr)
    Return a set of tuples (cmetab, cisostr, vmetab, visostr)
    where cmetab and cisostr describe a contex metabolite
    which combined with metab+isostr produced vmetab+visostr.
    if metab is alone on its reaction part cmetab and cisostr are set to
    an empty string ("").
    The set covers all combination of
    metab+isostr and its co-substrates which
    produce isotopes having at least one labeled carbon from
    metab+isostr.
    Co-substrate isotops are
    in a dictionary isos[cmetab]=list(cisotopes)."""
    # run through all combinations of srcs isotopes
    # to get all products
    # |isostr|*|iso2|[+|iso1|*|isostr| if metab is in both position]
    # If there is only one src than m2=""
    #print "allprods: metab=", metab, "isostr=", isostr
    #print "s=", srcs, "p=", prods, "i=", isos;##
    # find metabolite position(s) in the reaction
    mpos=[i for (i,(m,s)) in enumerate(srcs) if m==metab]
    
    # all source isotop couples (im,ic)
    icouples=[]
    if metab==m1:
        icouples=[(isostr,is2) for is2 in isos.get(m2,oset(("",)))]
    if metab==m2:
        icouples.extend((is1,isostr) for is1 in isos.get(m1,oset(("",))))
    #print "icpls=", list(icouples);##
    #sys.exit(1);##
    # labeled source letters
    #print "s1=", s1, "s2=", s2
    lets=["".join(l for (i,l) in enumerate(s1) if is1[i]=="1")+
        "".join(l for (i,l) in enumerate(s2) if is2[i]=="1")
        for (is1,is2) in icouples]
    #print "lets=", lets;##
    # form all products
    res=oset()
    for inm in mpos:
        (m,s)=srcs[inm]
        mlab="".join(l for (i,l) in enumerate(s) if isostr[i]=="1")
        # co-substrate (if any)
        (cm,cs)=("","") if len(srcs)==1 else srcs[(inm+1)%2]
        # run through isotops of co-substrate
        for coiso in isos.get(cm,oset(("",))):
            lab=mlab+"".join(l for (i,l) in enumerate(cs) if coiso[i]=="1")
            # form product isotops
            for (pm,ps) in prods:
                ip="".join(("1" if l in lab else "0") for l in ps)
                if oset(ps)&oset(mlab):
                    # get only products labeled by metab+isostr
                    res.add((cm,coiso, pm, ip))
    #print "res=", res
    return res
def prod(metab, iso, s, cmetab, ciso, cs, prods):
    """prod(metab, iso, s, cmetab, ciso, cs, prods)->oset()
    get isotops from labeled substrates"""
    #print "prod: m=", metab, "s=", s, "i=", iso, "cm=", cmetab, "cs=", cs, "ci=", ciso
    res=oset()
    if cs and ciso == -1:
        return res
    lab=""
    n=len(s)-1
    for (i,b) in iternumbit(iso):
        lab=lab+(s[n-i] if b else "")
    n=len(cs)-1
    for (i,b) in iternumbit(ciso):
        lab=lab+(cs[n-i] if b else "")
    for (pm,ps) in prods:
        ip=sum((1<<i if l in lab else 0) for (i,l) in enumerate(ps[::-1]))
        res.add((pm, ip))
    #print "res=", res
    return res
def frag_prod(metab, frag, s, cmetab, cfrag, cs, prods):
    """frag_prod(metab, frag, s, cmetab, cfrag, cs, prods)
    Get fragments from labeled substrates"""
    #print "frag_prod: m=", metab, "s=", s, "f=", frag, "cm=", cmetab, "cs=", cs, "cf=", cfrag
    res=oset()
    if cs and not cfrag:
        # no fragment for this metabolite is yet produced
        return res
    lab=""
    lab=("".join(s[i] for (i,c) in enumerate(frag) if c!="0")+
        "".join(cs[i] for (i,c) in enumerate(cfrag) if c!="0"))
    flab=frag.replace("0", "")+cfrag.replace("0", "")
    for (pm,ps) in prods:
        fp="".join((flab[lab.index(l)] if l in lab else "0") for (i,l) in enumerate(ps))
        res.add((pm, fp))
    #print "res=", res
    return res
def cumo_iw(w,nlen):
    """iterator for a given cumomer weight w in the carbon length nlen"""
    #print ("cumo_iw: w=%d,nlen=%d\n" % (w,nlen));##
    if w < 0 or w > nlen:
       raise Exception("CumoWeightOutOfRange")
    if nlen < 0:
       raise Exception("CumoLengthNegative")
    if w == 1:
        movbit=1
        for i in range(nlen):
            yield movbit
            movbit<<=1
    else:
        movbit=1<<(w-1)
        for i in range(nlen-w+1):
            for subi in cumo_iw(w-1,w+i-1):
                yield movbit+subi
            movbit<<=1

def iso2cumo(netan, strin, in_cumo, icumo, in_metab):
    """calculate cumomer fraction from isotopomer ones"""
    #return sum(iso_dic.get(iiso,0.)
    #    for iiso in icumo2iiso(icumo, Clen))
    w=sumbit(icumo)
    n=len(netan["iso_input"])
    if len(netan[strin]) != n:
        # init cumo_input list of dicts
        netan[strin]=[{} for i in range(n)]
    for ili in range(len(netan["iso_input"])):
        d=netan["iso_input"][ili][in_metab]
        netan[strin][ili][in_cumo]=sum(val for (iso, val) in d.items() if sumbit(iso&icumo) == w)

def iso2emu(netan, inmetab, mask, mpi, e):
    """calculate emu fraction from isotopomer dict iso_input.
    The fraction corresponds to a fragment defined by a mask and the mass component mpi.
    Return a real number in [0; 1] interval.
    """
    #return(sum([val for (frag, val) in iso_input[inmetab].iteritems() if sumbit(frag&mask) == mpi]))
    n=len(netan["iso_input"])
    if len(netan["emu_input"]) != n:
        # init emu_input list of dicts
        netan["emu_input"]=[{} for i in range(n)]
    for ili in range(n):
        d=netan["iso_input"][ili][inmetab]
        netan["emu_input"][ili][e]=sum([val for (frag, val) in d.items() if sumbit(frag&mask) == mpi])

def formula2dict(f, pterm=re.compile(r'([+-])'), pflux=re.compile(r'(?P<coef>\d+\.?\d*|^)?\s*\*?\s*(?P<var>[a-zA-Z_[\]()][\w\. -\[\]]*)\W*')):
    """parse a linear combination sum([+|-][a_i][*]f_i) where a_i is a 
    positive number and f_i is a string starting by non-digit and not white
    character (# is allowed). Output is a dict f_i:[+-]a_i"""
    res=dict()
    sign=1
    l=(i.strip() for i in pterm.split(str(f)))
    for term in l:
        try:
            next_sign=next(l)
            next_sign=-1 if next_sign == "-" else 1
        except StopIteration:
            next_sign=0
            pass
        if (len(term) == 0):
            continue
        m=pflux.match(term)
        if m:
            coef=m.group("coef")
            coef=1. if coef==None or not len(coef) else float(coef)
            var=m.group("var")
            sign=-1 if sign==-1 else 1
            res[var]=res.get(var, 0.)+sign*coef
            sign=next_sign
        else:
            raise Exception("Term '"+term+"' in formula '"+f+"' cannot be parsed.")
    return res
def mecoparse(terms, pmeco=re.compile(r'\s*((?P<coef>\d+\.?\d*|^)\s*\*\s*)?(?P<metab>[^*+ ]*)\s*$')):
    """Parse a string term from a list (or a sing string) of chemical equation entries.
    The general form of each term is 'coef*metab'.
    coef (if present) must be separated from metab by '*' and be convertible to float.
    metab can start with a number (e.g. '6PG') so the presence of '*' is mandatory to
    separate coef from metab. If coef is absent, it is considered to be 1. 
    Return a list of (or a single for str) tuples (metab (str), coef (real)).
    """
    single=isstr(terms)
    if single:
        terms=[terms]
    res=[]
    for term in terms:
        m=pmeco.match(term)
        if not m:
            raise ValueError("Wrong format for term '%s'"%term)
        coef=m.group("coef")
        coef=1. if coef==None or not len(coef) else float(coef)
        metab=m.group("metab")
        if not metab:
            raise ValueError("Metabolite cannot be empty in term '%s'"%term)
        res.append((metab, coef))
    if single:
        res=res.pop()
    return res

def label_meas2matrix_vec_dev(netan):
    """use netan["label_meas"] list to construct a corresponding list of
    measure matrix matx_lab
    such that scale_diag*metab_pool_diag*matx_lab*(cumos_vector,1) corresponds to label_measurements_vector.
    matx_lab is defined as
    list of dict{"scale":scale_name, "coefs":dict{icumo:coef}, "metab": metabolite, "poolid": metabolite pool id if pooled}
    where coef is a contribution of cumo in linear combination for given measure.
    scale_name is of the form "metabs;group". Group number is to group
    measurements of the same measurement set.
    poolid is the index of pool list in pooled where each list regroups
    0-based indexes rows in returned matrix for what has to be pooled together.
    vec is a list of measurements (values in .ftbl)
    dev is a list of deviations.
    Elements in matx_lab, vec and dev are ordered in the same way.
    The returned result is a dict (mat,vec,dev)
    """
    # lab[metab][group]=list of {id:txt, val:x, dev:y, bcumos:list of #bcumo}
    # bcumo is like #1xx01 (0,1 and x are allowed)
    # their sum have to be transformed in cumomer linear combination
    nexp=len(netan["iso_input"])
    mli=list(range(nexp))
    for ili in range(nexp):
        mat=[]
        vec=[]
        dev=[]
        pooled=[]
        ids=[]
        imrow=-1
        for (metabs,groups) in netan["label_meas"][ili].items():
            for (group,rows) in groups.items():
                for row in rows:
                    vec.append(row["val"])
                    dev.append(row["dev"])
                    ids.append(row["id"])
                    res=dict()
                    for cumostr in row["bcumos"]:
                        decomp=bcumo_decomp(cumostr)
                        for icumo in decomp["+"]:
                            res.setdefault(icumo,0)
                            res[icumo]+=1
                        for icumo in decomp["-"]:
                            res.setdefault(icumo,0)
                            res[icumo]-=1
                    emuco=dict((str(ic)+"+"+str(sumbit(ic)), c) for (ic, c) in res.items())
                    if len(row["pooled"]) > 1:
                        # init index list
                        pooled.append([row["id"]])
                    for metab in row["pooled"]:
                        imrow+=1
                        mat.append({"id": row["id"], "scale": metabs+";"+group, "coefs":res,
                            "bcumos": row["bcumos"], "metab": metab, "emuco": emuco})
                        if len(row["pooled"]) > 1:
                            # indeed something is pooled
                            pooled[-1].append(imrow)
        mli[ili]={"ids": ids, "mat": mat, "vec": vec, "dev": dev, "pooled": pooled}
    return mli

def mass_meas2matrix_vec_dev(netan):
    """use netan["mass_meas"] list to construct a corresponding list of
    measure matrix matx_mass
    such that scale_diag*matx_mass*cumos_vector corresponds to mass_measures_vector.
    matx_mass is defined as matx_lab in label_meas2matrix_vec_dev()
    Elements in matx_mass, vec and dev are ordered in the same way.
    scale name is defined as "metab;fragment_mask"
    The returned result is a dict (mat,vec,dev)
    """
    emu=netan["emu"]
    # mass[metab][frag_mask][weight]={val:x, dev:y}
    # weight has to be transformed in cumomer linear combination
    nexp=len(netan["iso_input"])
    mli=list(range(nexp))
    for ili in range(nexp):
        mat=[]
        vec=[]
        dev=[]
        pooled=[]
        ids=[]
        imrow=-1
        for (m_id,frag_masks) in netan["mass_meas"][ili].items():
            #print "mass matx calc for ", metab;##
            (l, metabs, frag, m_irow)=m_id.split(":")
            for (fmask,weights) in frag_masks.items():
                onepos=[b_no for (b_no,b) in iternumbit(fmask) if b]
                nmask=sumbit(fmask)
    #            aff("weights for met,fmask "+", ".join((metab,strbit(fmask))), weights);##
                for (weight,row) in weights.items():
                    vec.append(row["val"])
                    dev.append(row["dev"])
                    ids.append(row["id"])
                    metabl=row["pooled"]
                    metab0=metabl[0]
                    n=netan["Clen"][metab0]
                    str0="0"*n
                    strx="x"*n
                    str1="1"*n
                    fmask0x=setcharbit(strx,"0",fmask)
                    fmask01=setcharbit(str0,"1",fmask)
                    if not emu:
                        # for a given weight construct bcumo sum: #x10x+#x01x+...
                        bcumos=["#"+setcharbit(fmask0x,"1",expandbit(iw,onepos))
                                for iw in range(1<<nmask) if sumbit(iw)==weight]
        #                aff("bcumos for met,fmask,w "+", ".join((metab,strbit(fmask),str(weight))), [b for b in bcumos]);##
                        res=dict()
                        for cumostr in bcumos:
                            #print cumostr;##
                            decomp=bcumo_decomp(cumostr)
                            for icumo in decomp["+"]:
                                res.setdefault(icumo,0)
                                res[icumo]+=1
                            for icumo in decomp["-"]:
                                res.setdefault(icumo,0)
                                res[icumo]-=1
                    else:
                        bcumos=None
                        res=None
                    emuco={str(fmask)+"+"+str(weight): 1}
                    if len(row["pooled"]) > 1:
                        # init index list
                        pooled.append([row["id"]])
                    for metab in row["pooled"]:
                        imrow+=1
                        if len(row["pooled"]) > 1:
                            # indeed something is pooled
                            pooled[-1].append(imrow)
                        mat.append({"id": row["id"], "scale": metabs+";"+fmask01+";"+m_irow, "coefs":res,
                            "bcumos": bcumos, "metab": metab, "emuco": emuco})
        mli[ili]={"ids": ids, "mat": mat, "vec": vec, "dev": dev, "pooled": pooled}
    return mli

def peak_meas2matrix_vec_dev(netan, dmask={"S": 2, "D-": 6, "D+": 3, "T": 7, "DD": 7}):
    """use netan["peak_meas"] list to construct a corresponding list of
    measure matrix matx_peak
    such that scale_diag*matx_peak*cumos_vector corresponds to
    peak_measures_vector.
    dmask is a dictionary with 3 carbon labeling pattern mask for
    various peak types. The middle bit corresponds to the targeted carbon,
    lower bit corresponds to the next neighbour (D+) and higher bit
    corresponds to previous carbon (D-).
    matx_peak is defined as matx_lab in peak_meas2matrix_vec_dev()
    Elements in matx_peak, vec and dev are ordered in the same way.
    scale name is defined as "metab;c_no;irow"
    The returned result is a dict (mat,vec,dev)
    """
    # peak[metab][c_no][peak_type]={val:x, dev:y}
    # c_no+peak_type have to be transformed in cumomer linear combination
    # c_no is 1-based and left (just after # sign) started.
    nexp=len(netan["iso_input"])
    mli=list(range(nexp))
    for ili in range(nexp):
        mat=[]
        vec=[]
        dev=[]
        pooled=[]
        ids=[]
        imrow=-1
        for (metabs,irows) in netan["peak_meas"][0].items():
            #print "peak matx calc for ", metab;##
            #import pdb; pdb.set_trace()
            metabl=[*[*irows.values()][0].values()][0]["pooled"]
            #print(metabl)
            #metabl=metabl["pooled"]
            
            metab0=metabl[0]
            n=netan["Clen"][metab0]
            strx="#"+"x"*n
            for (irow,peaks) in irows.items():
                # pmask0x put 0 on three targeted carbons and leave x elsewhere
                c_no=[*peaks.values()][0]["c_no"]
                pmask0x=setcharbit(strx,"0",1<<(n-c_no))
                if c_no > 1:
                    # add left neighbour
                    pmask0x=setcharbit(pmask0x,"0",1<<(n-c_no+1))
                if c_no < n:
                    # add right neighbour
                    pmask0x=setcharbit(pmask0x,"0",1<<(n-c_no-1))
    #            aff("peaks for met,c_no,pmask0x="+", ".join((metab,str(c_no),pmask0x)), peaks);##
                for (peak,row) in peaks.items():
                    vec.append(row["val"])
                    dev.append(row["dev"])
                    ids.append(row["id"])
                    res=dict()
                    # for a given (c_no,peak) construct bcumo sum: #x10x+#x01x+...
                    # shift the 3-bit mask to the right carbon position
                    if c_no < n:
                        pmask=dmask[peak]<<(n-c_no-1)
                    else:
                        pmask=dmask[peak]>>1
                    bcumos=[setcharbit(pmask0x,"1",pmask)]
                    if peak=="D-" and "T" in peaks:
                        # D+===D- => add D+ to the list of measured bcumomers
                        # set the 3-bit mask to the right carbon position
                        if c_no < n:
                            pmask=dmask["D+"]<<(n-c_no-1)
                        else:
                            pmask=dmask["D+"]>>1
                        bcumos.append(setcharbit(pmask0x,"1",pmask))
    #                aff("bcumos for met,fmask,w "+", ".join((metab,strbit(fmask),str(weight))), [b for b in bcumos]);##
                    for cumostr in bcumos:
                        #print cumostr;##
                        decomp=bcumo_decomp(cumostr)
                        for icumo in decomp["+"]:
                            res.setdefault(icumo,0)
                            res[icumo]+=1
                        for icumo in decomp["-"]:
                            res.setdefault(icumo,0)
                            res[icumo]-=1
                    emuco=dict((str(ic)+"+"+str(sumbit(ic)), c) for (ic, c) in res.items())
                    if len(row["pooled"]) > 1:
                        # init index list
                        pooled.append([row["id"]])
                    for metab in row["pooled"]:
                        imrow+=1
                        mat.append({"id": row["id"], "scale": metabs+";"+str(c_no)+";"+row["irow"], "coefs":res,
                            "bcumos": bcumos, "metab": metab, "emuco": emuco})
                        if len(row["pooled"]) > 1:
                            # indeed something is pooled
                            pooled[-1].append(imrow)
                    #print "metab,c_no,peak="+",".join((metab,str(c_no),str(peak)));##
                    #print "res=",str(res);##
        mli[ili]={"ids": ids, "mat": mat, "vec": vec, "dev": dev, "pooled": pooled}
    return mli

def bcumo_decomp(bcumo):
    """bcumo is a string of the form #[01x]+. It has to be decomposed
    in the linear combination of cumomers #[1x]+. The coefficients
    of this linear combination are 1 or -1. So it can be represented as
    sum(cumos_positive)-sum(cumos_negative). The result of this function
    is a dictionary {"+": list of icumos, "-": list of icumos}. icumo is
    an integer whose binary form indicates 1's positions in a cumomer."""
    
    # take zero positions
    zpos=[ i for i in range(len(bcumo)-1) if bcumo[-i-1]=="0" ]
    
    # basic 1's cumomer mask
    icumo_base=0
    for i in range(len(bcumo)-1):
        if bcumo[-i-1]=="1":
            icumo_base|=1<<i
    
    # run through all 2^n zero masks separating positive and negative
    # cumomers
    res={"+": [], "-": []}
    nz=len(zpos)
    for zmask in range(1<<nz):
        # odd or even bit number?
        sign="-" if sumbit(zmask)%2 else "+"
        
        # get full cumomer mask
        icumo=icumo_base|expandbit(zmask,zpos)
        # put in result dict
        res[sign].append(icumo)
    #print bcumo+"=" + "+".join(setcharbit("x"*len(bcumo),"1",i) for i in res["+"])+("-" if res["-"] else "") +"-".join(setcharbit("x"*len(bcumo),"1",i) for i in res["-"]);##
    return res
def mat2graph(A, fp):
    """write digraph file on file pointer fp representing
    links in matrix A given as bi-level dictionnary. A key of
    first level (row index) is influenced by keys of second level
    (column indicies)."""
    fp.write("digraph A {\n")
    for i in A:
        labi=i.replace(":", "_")
        for j in A[i]:
            if i==j:
                continue
            labj=j.replace(":", "_")
            fp.write("   \""+labj+"\" -> \""+labi+"\"\n")
    fp.write("}\n")
def dom_cmp(A, i, j):
    """Compares influances of i-th and j-th lements of A.
    Returns 0 if i and j are mutually influenced, 1 if i in A[j]
    (i influences j) , -1 otherwise"""
    return (0 if i in A[j] and j in A[i] else 1 if i in A[j] else -1 if j in A[i] else 0)
def mat2pbm(A, v, fp):
    """Write an image map of non-zero entries of matrix A to file pointer fp.
    Matrix A is a dictionnary, v is a list ordering keys of A."""
    fp.write("P1\n%d %d\n"% (len(A), len(A)))
    for i in v:
        p=0
        for j in v:
            fp.write("1 " if j in A[i] else "0 ")
            p+=2
            if p >= 69:
                fp.write("\n")
                p=0
        fp.write("\n")
        p=0
def aglom_loop1(A):
    """Agglomerate nodes of A if they mutually influence each other
    i.e. they are in a loop of length 1.
    Return a new dictionary of influence where entries are those of A aglomerated and glued "by" tab symbol"""
    # i->loop_name(=min of all nodes in this loop)
    na=copy.deepcopy(A)
    loop=dict()
    while aglom(na,transpose(na),loop):
        pass
    return({"na": na, "loop":loop})
def aglom(na,ta,loop):
    """new matrix A (na), transpose A (ta) are used to
    aglomerate neigbour mutually influencing nodes in a supernode.
    Aglomerated noeds are put in the loop dictionnary.
    Return False if no nodes were aglomerated.
    """
    found=False
    for i in na:
        for j in na[i]:
            if j==i:
                continue
            if i in na[j]:
                # agglomerate i and j in na
                # which means keep lowest netween i and j
                # lo=min(i,j)
                # hi=max(i,j)
                # and imports influences of hi to lo
                found=True
                lo=min(i,j)
                hi=max(i,j)
                del(na[hi][hi])
                del(ta[hi][hi])
                #print "aglom: "+i+"+"+j+"->"+lo;##
                #print "elim row "+hi+str(na[hi].keys());##
                #print "influenced rows "+str(ta[hi].keys());##
                # update na rows influenced by hi
                for ii in [*ta[hi].keys()]:
                    #print "\nold row "+ii+str(na[ii].keys());##
                    na[ii].update(na[hi])
                    del(na[ii][hi])
                    for jj in na[ii]:
                        ta[jj][ii]=na[ii][jj]
                    #print "new row "+ii+str(na[ii].keys());##
                # remove na[hi], ta[hi] and ta's corresponding to na[hi]
                for ii in na[hi]:
                    del(ta[ii][hi])
                del(na[hi])
                del(ta[hi])
                # update loop dictionary
                loop[lo]=loop.get(lo,oset((lo,)))
                loop[lo].update(loop.get(hi, oset((hi,))))
                if hi in loop:
                    del(loop[hi])
                #print "loop:"+str(loop)
                return(found)
def lowtri(A):
    """Try low triangular ordering of matrix A entries"""
    unsrt=[*A.keys()]
    srt=list()
    # first get inputs (no influences on them)
    srt=[k for k in unsrt if len(A[k])==1]
    for k in srt:
        unsrt.remove(k)
    # now move to lower number keys that influence others
    while unsrt:
        for k in list(unsrt):
            srt.extend(oset(A[k].keys()).difference(oset(srt)))
            for i in [*A[k].keys()]:
                try:
                    unsrt.remove(i)
                except ValueError:
                    pass
    return srt
def topo_order(A, tA):
    """Try to sort keys of A in topological order. tA is just a transpose of A"""
    unsrt=oset(A.keys())
    srtin=list()
    srtout=list()
    
    while unsrt:
        # shave inputs and outputs
        inps=oset(k for k in unsrt if len(oset(A[k]).difference(srtin))==1)
        srtin.extend(inps)
        unsrt.difference_update(inps)
        outs=oset(k for k in unsrt if len(oset(tA[k]).difference(unsrt))==1)
        if outs:
            srtout.insert(0, outs)
            unsrt.difference_update(outs)
        if not inps and not outs:
            # this is not a dag
            srtin.extend(unsrt)
            #print "not a dag"
            break
    return(srtin+srtout)
def transpose(A):
    """Transpose a matrix defined as a dict."""
    tA=dict()
    for i in A:
        for j in A[i]:
            tA[j]=tA.get(j, dict())
            tA[j][i]=A[i][j]
    return(tA)
def rcumo_sys(netan, emu=False):
    """Calculate reduced cumomers or EMU systems A*x=b
    we start with observed cumomers (emus) of max weight
    and we include only needed involved cumomers (emus)
    A list of cumomer (emu) lists (by weight) is stored
    in netan["vrcumo"] (netan["vemu"])
    """
    # generate measurements dico if not yet done
    if "measures" not in netan:
        measures=dict()
        for meas in ("label", "mass", "peak"):
            measures[meas]=eval("%s_meas2matrix_vec_dev(netan)"%meas)
        netan["measures"]=measures
    measures=netan["measures"]

    # init rcumo_input
    n=len(netan["iso_input"])
    if len(netan["rcumo_input"]) != n:
        # init rcumo_input list of dicts
        netan["rcumo_input"]=[{} for i in range(n)]
    
    # get cumomers involved in measurements
    meas_cumos=oset()
    if emu:
        for meas in measures:
            for item in measures[meas]:
                for row in item["mat"]:
                    metab=row["metab"]
                    meas_cumos.update(metab+":"+i.split("+")[0] for i in [*row["emuco"].keys()]) # if i[-2:]!="+0")
    else:
        for meas in measures:
            for item in measures[meas]:
                for row in item["mat"]:
                    metab=row["metab"]
                    meas_cumos.update(metab+":"+str(icumo) for icumo in [*row["coefs"].keys()] if icumo != 0)
    # make list of observed weights
    weights=oset(sumbit(int(cumo.split(":")[1])) for cumo in meas_cumos)
    if not weights:
        netan["vrcumo"]=[]
        netan["rcumo2i0"]=dict()
        netan["rcumo_sys"]={"A": [], "b": []}
        return {"A": [], "b": []}
    
    maxw=max(weights)
    weights=list(range(1,maxw+1))
    weights.reverse()
    
    # cumomers to visit are stored in lists by weights
    to_visit=dict((w,[]) for w in weights)
    A=[{} for i in range(maxw)]; # store matrices by weight
    b=[{} for i in range(maxw)]; # store rhs by weight
    used=oset()

    # initialize to_visit, we'll stop when it's empty
    for cumo in meas_cumos:
        (m,w)=cumo.split(":")
        if m in netan["input"]:
            continue
        w=sumbit(int(w))
        to_visit[w].append(cumo)

    # run through the network starting with heaviest cumomers
    for w in weights:
        cumo_i=0
        while cumo_i < len(to_visit[w]):
            for cumo in to_visit[w]:
                cumo_i+=1
                if cumo in used:
                    continue
                # add this cumo to used
                used.add(cumo)
                (metab, icumo)=cumo.split(":")
                if metab in netan["output"]:
                    # no equation for output metabs
                    continue
                icumo=int(icumo)
                A[w-1][cumo]=A[w-1].get(cumo,{cumo:[]})
                # get the influents to cumo of all weights: equal and lower
                # equals go to A and lowers go to b
                infl=cumo_infl(netan, cumo)
                #print("cumo=", cumo, "infl=", infl)
                for (incumo,fl,imetab,iinmetab) in infl:
                    if incumo==cumo:
                        # no equation as no transformation
                        continue
                    (inmetab, inw)=incumo.split(":")
                    inicumo=int(inw)
                    inw=sumbit(inicumo)
                    # input metabolites are to rhs, others are to visit
                    if inmetab not in netan["input"] and inw != 0 and incumo not in to_visit[inw]:
                        to_visit[inw].append(incumo)
                    if inmetab in netan["input"]:
                        if emu:
                            # tuple emu (mask, m+i, string)
                            for mask, mpi, e in ((inicumo, i, incumo+"+"+str(i)) for i in range(inw+1)):
                                iso2emu(netan, inmetab, mask, mpi, e)
                        if not netan["rcumo_input"] or incumo not in netan["rcumo_input"][0]:
                            iso2cumo(netan, "rcumo_input", incumo, inicumo, inmetab)
                        #netan["rcumo_input"][incumo]=netan["cumo_input"][incumo]
                    # main part: write equations
                    if inw==w :
                        # equal weight => A
                        if inmetab in netan["input"]:
                            b[w-1][cumo]=b[w-1].get(cumo, dict())
                            b[w-1][cumo][fl]=b[w-1][cumo].get(fl, dict())
                            b[w-1][cumo][fl][imetab]=b[w-1][cumo][fl].get(imetab,[])
                            #b[w-1][cumo][fl][imetab].append(netan["cumo_input"][incumo])
                            b[w-1][cumo][fl][imetab].append(incumo)
                            continue
                        A[w-1][cumo][incumo]=A[w-1][cumo].get(incumo,[])
                        A[w-1][cumo][incumo].append(fl)
                        #aff("A "+str(w)+cumo, A[w-1][cumo]);##
                    elif inw < w:
                        # lower weight => b
                        b[w-1][cumo]=b[w-1].get(cumo, dict())
                        b[w-1][cumo][fl]=b[w-1][cumo].get(fl, dict())
                        b[w-1][cumo][fl][imetab]=b[w-1][cumo][fl].get(imetab,[])
                        b[w-1][cumo][fl][imetab].append(incumo)
                # gather all influx in diagonal term
                #aff("A "+str(w)+" "+cumo, A[w-1][cumo]);##
                #aff("b "+str(w)+" "+cumo, b[w-1].get(cumo));##
                for incumo in A[w-1][cumo]:
                    if incumo == cumo:
                        continue
                    A[w-1][cumo]=A[w-1].get(cumo,{cumo:[]})
                    #print("adding A:", incumo, "->", cumo, A[w-1][cumo][incumo]);##
                    A[w-1][cumo][cumo]+=A[w-1][cumo][incumo]
                if cumo in b[w-1]:
                    A[w-1][cumo]=A[w-1].get(cumo,{cumo:[]})
                    #print("adding b:", cumo, b[w-1][cumo].keys());##
                    A[w-1][cumo][cumo]+=[*b[w-1][cumo].keys()]
    #import pdb; pdb.set_trace()
    #netan["rcumo_input"]=[dict((k, (v if v==v else 0.)) for k,v in d.items()) for d in netan["rcumo_input"]]
    #aff("to_v", to_visit);##
    # make ordered list for reduced cumomer set
    netan["vrcumo"]=[[*a.keys()] for a in A]
    netan["rcumo2i0"]=dict((cumo,i) for (i, cumo) in enumerate(valval(netan["vrcumo"])))
    #print("A=", A)
    netan["rcumo_sys"]={"A": A, "b": b}
    # make order list for emu_input
    if emu:
        netan["vemu_input"]=[*netan["emu_input"][0].keys()]
        # make order list for internal emus
        netan["vemu"]=copy.deepcopy(netan["vrcumo"])
        for w in range(len(netan["vemu"])):
            l=netan["vemu"][w]
            netan["vemu"][w]=[m+"+"+str(i) for i in range(w+2) for m in l]
        netan["emu2i0"]=dict((emu,i) for (i, emu) in enumerate(valval(netan["vemu"])))
    return netan["rcumo_sys"]

def cumo_infl(netan, cumo):
    """cumo_infl(netan, cumo)->list(tuple(in_cumo, fl, imetab, iin_metab))
    return the list of tuples (in_cumo, fl, imetab, iin_metab):
    input cumomer, flux (fwd.fl or rev.fl), index of metab and index of in_metab
    generating cumo. cumo is in format "metab:icumo".
    Condenstation reaction will give the same flux and icumo but various
    iin_metab.
    Convergent point will give multiple fluxes.
    """
    (metab, icumo)=cumo.split(":")
    icumo=int(icumo)
    res=[]
    # run through input forward fluxes of this metab
    for reac,coef in oset(netan["sto_m_r"][metab]["right"]):
        if reac not in netan["carbotrans"]:
            continue # it can happen because of NOTRACER_NETWORK
        # get all cstr for given metab
        for (imetab,cstr) in ((i,s) for (i,(m,s)) in enumerate(netan["carbotrans"][reac]["right"])
            if m==metab):
            # get all input cumomer in this reaction for this cstr
            for (iin_metab, (in_metab,in_str)) in enumerate(netan["carbotrans"][reac]["left"]):
                in_icumo=src_ind(in_str, cstr, icumo)
                if in_icumo != None:
                    in_cumo=in_metab+":"+str(in_icumo)
                    res.append((in_cumo, "fwd."+reac, imetab, iin_metab))
    # run through input reverse fluxes of this metab
    fluxset=oset(f for f,c in netan["sto_m_r"][metab]["left"])
    if (clownr):
        # non reversible reactions are all positive
        fluxset=fluxset.difference(netan["notrev"])
    else:
        # non reversible reactions can change sens => keep reverse flux just in case
        fluxset=fluxset.difference(netan["flux_inout"])
    for reac in fluxset:
        # get all cstr for given metab
        if reac not in netan["carbotrans"]:
            continue
        for (imetab,cstr) in ((i,s) for (i,(m,s)) in enumerate(netan["carbotrans"][reac]["left"])
            if m==metab):
            # get all input cumomer in this reaction for this cstr
            for (iin_metab, (in_metab,in_str)) in enumerate(netan["carbotrans"][reac]["right"]):
                in_icumo=src_ind(in_str, cstr, icumo)
                if in_icumo != None:
                    in_cumo=in_metab+":"+str(in_icumo)
                    res.append((in_cumo, "rev."+reac, imetab, iin_metab))
    return res
def infl(metab, netan):
    """infl(metab, netan)->oset(fluxes)
    List incoming fluxes for this metabolite (fwd.reac, rev.reac, ...)
    """
    # run through input forward fluxes of this metab
    res=oset("fwd."+reac for reac,_ in netan["sto_m_r"][metab]["right"])
    # run through input reverse fluxes of this metab
    res.update("rev."+reac for reac in
        oset(f for f,_ in netan["sto_m_r"][metab]["left"]).difference(netan["flux_inout"]))
#        oset(netan["sto_m_r"][metab]["left"]).difference(netan["notrev"]))
    return(res)

def t_iso2m(n):
    """t_iso2m(n) return transition matrix from isotopomers fractions to MID vector
    n - carbon number
    return numpy array of size (n+1,2**n)
    """
    # isotopomer number
    ni=2**n
    return np.array(
        [[1. if sumbit(ii)==im else 0 for ii in range(ni)] for im in range(n+1)]
    )

def t_iso2cumo(n):
    """t_iso2cumo(n) return transition matrix from isotopomers fractions to cumomer vector
    n - carbon number
    return numpy array of size (2**n,2**n)
    """
    if n <= 0:
        return np.array(1, ndmin=2)
    # recursive call
    m_1=t_iso2cumo(n-1)
    nc1,nc1=m_1.shape
    nc=2*nc1
    m=np.zeros((nc,nc))
    m[0:nc1,0:nc1],m[0:nc1,nc1:],m[nc1:,nc1:]=m_1,m_1,m_1
    return m

def t_iso2pos(n):
    """t_iso2pos(n) return transition matrix from isotopomers fractions to positional
    labelling vector (cumomers of weight 1)
    n - carbon number
    return numpy array of size (n,2**n)
    """
    m=t_iso2cumo(n)
    return m[[1<<i for i in range(n)],:]

def conv_mid(x,y):
    """conv_mid(x,y)->z
    convolute two mid vectors (numpy arrays)
    and return the result as numpy array.
    """
    nx=len(x)
    ny=len(y)
    z=np.zeros(nx+ny-1)
    for i in range(ny):
        z[i:i+nx]+=x*y[i]
    return(z)

def ms_frag_gath(netan):
    """gather metabolite fragments necessary to obtain a given set of data
    observed in MS measurements.
    The fragment mask is encoded in the same way as cumomers, Met:7 <=> Met#(0)111
    """
    frags=oset()
    to_visit=oset()
    for di in netan["mass_meas"]:
        for m_id in di:
            for mask in di[m_id]:
                # just the first weight item is sufficient
                item=[*di[m_id][mask].values()][0]
                to_visit.update(met+":"+str(mask) for met in item["pooled"])
    while to_visit:
        for frag in list(to_visit):
            if frag in frags:
                # already accounted
                to_visit.remove(frag)
                continue
            frags.add(frag)
            to_visit.remove(frag)
            # add its contributors for visiting
            to_visit.update(incumo for (incumo,fl,imetab,iinmetab) in cumo_infl(netan, frag))
            break
    return(frags)
def ntimes(n):
    """Return charcater string 'once' for n=1, 'twice' for n=2 and 'n times' for other n"""
    return("once" if n==1 else "twice" if n==2 else "%d times"%n)
def proc_label_input(ftbl, netan, case_i=False):
    """Proceed LABEL_INPUT section in ftbl and add result to the list netan["iso_input"] and netan["funlab"] (case_i)
    List item is a dict {metab;{isotop_int_index:fraction}}
    """
    ili=len(netan["iso_input"]) # label_input list index
    netan["iso_input"]+=[{}]
    if case_i:
        netan["funlab"]+=[{}]
        resf=netan["funlab"][ili]
    res=netan["iso_input"][ili]
    # input isotopomers
    #import pdb; pdb.set_trace()
    for row in ftbl.get("LABEL_INPUT",[]):
        metab=row.get("META_NAME", "") or metab
        if metab not in netan["Clen"]:
            raise Exception("Label input metabolite `%s` is not defined in NETWORK (%s: %s)."%(metab, ftbl["name"], row["irow"]))
        ilen=len(row.get("ISOTOPOMER", ""))-1; # -1 for '#' sign
        if ilen != netan["Clen"][metab]:
            raise Exception("Input isotopomer `%s` is of bad length (%d). A length of %d is expected (%s: %s)."%
                (row.get("META_NAME", "")+row.get("ISOTOPOMER", ""), ilen,  netan["Clen"][metab], ftbl["name"], row["irow"]))
        iiso=strbit2int(row["ISOTOPOMER"])
        res[metab]=res.get(metab, {})
        if case_i:
            resf[metab]=resf.get(metab, {})
            resf[metab][iiso]=row["VALUE"]
            res[metab][iiso]=NA
        else:
            val=eval_expr(row["VALUE"])
            if val is None:
                val=row["VALUE"]
            if type(val) in (float, int):
                if val < 0. and val >= -tol:
                    val=0.
                if val > 1. and val <= 1.+tol:
                    val=1.
                if val < 0 or val > 1:
                    raise ValueError("Input isotopomer `%s` has a value (%g) out of range [0; 1] (%s: %s)"%(row.get("META_NAME", "")+row.get("ISOTOPOMER", ""), val, ftbl["name"], row["irow"]))
            res[metab][iiso]=val
    # check that all isoforms sum up to 1 for all inputs for influx_s
    if not case_i:
        for metab in res:
            le=len(res[metab])
            nfo=2**netan["Clen"][metab]
            su=sum(res[metab].values())
            if su > 1.+1.e-10:
                raise Exception("Input metabolite `%s` has label summing up to %g which is greater than 1 (%s)."%(metab, su, ftbl["name"]))
            if le == nfo:
                # all forms are given => must sum up to 1
                if abs(su-1.) > 1.e-10:
                    raise Exception("Input metabolite `%s` has label summing up to %g instead of 1 (%s)."%(metab, su, ftbl["name"]))
            elif le < nfo-1:
                # many forms are lacking (not just one)
                # if fully unlabeled is lacking, use it to complete to 1
                # otherwise raise an error
                if 0 not in res[metab]:
                    res[metab][0]=1.-su
                elif abs(su-1.) > 1.e-10:
                    raise Exception("Input metabolite `%s` has lacking labels to sum up to 1 (we get sum=%g instead) (%s)."%(metab, su, ftbl["name"]))
            elif le == nfo-1:
                # just one form is lacking
                if su != 1.:
                    # add it to complete to 1
                    la=list(oset(range(nfo))-oset(res[metab]))[0]
                    res[metab][la]=1.-su
    if ili > 0:
        # complete absent inputs by their values from the first ftbl
        for m in oset(netan["iso_input"][0])-oset(res):
            res[m]=netan["iso_input"][0][m];
        if case_i:
            for m in oset(netan["funlab"][0])-oset(resf):
                resf[m]=netan["funlab"][0][m];
    zeroc=oset(m for m,n in netan["Clen"].items() if n == 0)
    lmi=oset(res.keys())-netan["input"] # label input - input
    iml=oset(netan["input"])-zeroc-oset(res.keys()) # input - label input
    if lmi:
        raise Exception("LABEL_INPUT section contains metabolite(s) that are not network input(s): '%s' (%s)\n"%(", ".join(lmi), ftbl["name"]))
    if iml:
        raise Exception("LABEL_INPUT section lacks certain metabolite(s) that are network input(s): '%s' (%s)\n"%(", ".join(iml), ftbl["name"]))

def proc_label_meas(ftbl, netan):
    """Proceed LABEL_MEASUREMENT section of ftbl file, add the result to a list of dicts
    """
    # label measurements
    # [ili]][metab][group]->list of {val:x, dev:y, bcumos:list of #bcumo}
    ili=len(netan["label_meas"])
    netan["label_meas"]+=[{}]
    res=netan["label_meas"][ili]
    
    metabs=""
    for row in ftbl.get("LABEL_MEASUREMENTS", []):
        #print row;##
        # test the cumomer pattern validity
        if (not re.match(r"#[01x]+(\+#[01x]+)*", row["CUM_CONSTRAINTS"])):
            raise Exception("Not valid cumomer's pattern in '"+row["CUM_CONSTRAINTS"]+"' (%s: %s)"%(ftbl["name"], row["irow"]))
        metabs=row["META_NAME"] or metabs
        group=row["CUM_GROUP"] or group
        # metabs can be metab1[+metab2[+...]]
        metabl=[v.strip() for v in metabs.split("+")]
        if (len(metabl) > 1):
            # pooling metabolites will need their concentraions
            mdif=oset(metabl).difference(netan["met_pools"])
            if mdif:
                raise Exception("Pooled metabolite(s) '%s' are absent in METABOLITE_POOLS section (%s: %s)."%(join(", ", mdif), ftbl["name"], row["irow"]))

        # check that all metabs are unique
        count=dict((i, metabl.count(i)) for i in oset(metabl))
        notuniq=oset((i for i in count if count[i] > 1))
        if notuniq:
            raise Exception("Metabolite(s) '%s' is present twice or more (%s: %s)"%(join(", ", notuniq), ftbl["name"], row["irow"]))

        metab0=metabl[0] if metabl else ""
        if not metab0 in netan["metabs"]:
            raise Exception("Unknown metabolite name '%s' in LABEL_MEASUREMENTS (%s: %s)"%(metab0, ftbl["name"], row["irow"]))
        clen0=netan["Clen"][metab0] if metab0 else 0
        for metab in metabl:
            if not metab in netan["metabs"]:
                raise Exception("Unknown metabolite name '%s' in LABEL_MEASUREMENTS (%s: %s)"%(metab, ftbl["name"], row["irow"]))
            if metab in netan["output"] or metab in netan["input"]:
                raise Exception("""Measured metabolites have to be internal to network (found in input or output metabolites of '%s').
In case of output, you can add a fictitious metabolite in your network immediatly after '"""%ftbl["name"]+metab+"' (seen in LABEL_MEASUREMENTS).")
            mlen=netan["Clen"][metab]
            clen=netan["Clen"][metab]
            if clen!=clen0 :
                raise Exception("Carbon length of '%s' (%d) is different from the length of '%s' (%d) (%s: %s)"%(metab, clen, metab0, clen0, ftbl["name"], row["irow"]))
        if not metabs in res:
            res[metabs]=dict()
        if not group in res[metabs]:
            res[metabs][group]=[]
        # prepare cumos list
        # if bcumos is not empty use it
        # else use group name as carbon number (starting from # character)
        if row.get("CUM_CONSTRAINTS",""):
            bcumos=[v.replace("#", "") for v in row["CUM_CONSTRAINTS"].split("+")]
        else:
            try:
                # just put "1" in group-th place
                i=int(group)
                bcumos="x"*netan["Clen"][metabl[0]]
                bcumos[i-1]="1"
                bcumos=[bcumos]
            except ValueError:
                raise Exception("Expected integer CUM_GROUP in LABEL_MEASUREMENTS on row "+row["irow"])
        if not row["VALUE"]:
            raise ValueError("The field VALUE is empty (%s: %s)"%(ftbl["name"], row["irow"]))
        if not row["DEVIATION"]:
            raise ValueError("The field DEVIATION is empty (%s: %s)"%(ftbl["name"], row["irow"]))
        try:
            val=float(eval_expr(row["VALUE"]))
        except ValueError:
            val=NaN
        try:
            sdev=float(eval_expr(row["DEVIATION"]))
        except ValueError:
            raise ValueError("DEVIATION must evaluate to a real positive number (%s: %s)."%(ftbl["name"], row["irow"]))
        if sdev <= 0.:
            raise ValueError("DEVIATION must be positive (%s: %s)."%(ftbl["name"], row["irow"]))
        res[metabs][group].append({
                "val":val,
                "dev":sdev,
                "bcumos":row["CUM_CONSTRAINTS"].split("+"),
                "id":":".join(["l", metabs, row["CUM_CONSTRAINTS"].replace("#", ""), str(row["irow"])]),
                "pooled":metabl,
        })
        # test the icumomer lengths
        if not all(len(ic)==mlen+1 for ic in 
                res[metabs][group][-1]["bcumos"]):
            raise Exception("Wrong cumomer length for %s in %s (%s: %s)"%(metab, row["CUM_CONSTRAINTS"], ftbl["name"], row["irow"]))
def proc_peak_meas(ftbl, netan):
    """Proceed PEAK_MEASUREMENT section of ftbl file, add the result to a list of dicts
    """
    # peak measurements
    # [ili]][metab][c_no][peak_type in (S,D-,D+,(DD|T))]={val:x, dev:y}
    ili=len(netan["peak_meas"])
    netan["peak_meas"]+=[{}]
    res=netan["peak_meas"][ili]
    # peak measurements
    metabs=""
    for row in ftbl.get("PEAK_MEASUREMENTS",[]):
        #print row;##
        # test the pattern validity
        if (row.get("VALUE_DD","") and row.get("VALUE_T","")):
            raise Exception("Not valid value combination. Only one of DD and T has to be in row "+str(row)+" (%s: %s)"%(ftbl["name"], row["irow"]))
        metabs=row["META_NAME"] or metabs
        metabl=[v.strip() for v in metabs.split("+")]
        if (len(metabl) > 1):
            # pooling metabolites will need their concentraions
            mdif=oset(metabl).difference(netan["met_pools"])
            if mdif:
                raise Exception("Pooled metabolite(s) '%s' are absent in METABOLITE_POOLS section (%s: %s)"%(join(", ", mdif), ftbl["name"], row["irow"]))

        # check that all metabs are unique
        count=dict((i, metabl.count(i)) for i in oset(metabl))
        notuniq=oset((i for i in count if count[i] > 1))
        if notuniq:
            raise Exception("Metabolite(s) '%s' is present twice or more (%s: %s)"%(join(", ", notuniq), ftbl["name"], row["irow"]))

        metab0=metabl[0] if metabl else ""
        if not metab0 in netan["metabs"]:
            raise Exception("Unknown metabolite name '%s' in PEAK_MEASUREMENTS (%s: %s)"%(metab0, ftbl["name"], row["irow"]))
        clen0=netan["Clen"][metab0] if metab0 else 0
        for metab in metabl:
            if not metab in netan["metabs"]:
                raise Exception("Unknown metabolite name '%s' in PEAK_MEASUREMENTS (%s: %s)"%(metab, ftbl["name"], row["irow"]))
            if metab in netan["output"] or metab in netan["input"]:
                raise Exception("""Measured metabolites have to be internal to network (seen in input or output metabolites (%s: %d) ).
In case of output, you can add a fictitious metabolite in your network immediatly after '"""%(ftbl["name"], row["irow"])+metab+"' (seen in PEAK_MEASUREMENTS).")
            clen=netan["Clen"][metab]
            if clen!=clen0 :
                raise Exception("Carbon length of '%s' (%d) is different from the length of '%s' (%d) (%s: %s)"%(metab, clen, metab0, clen0, ftbl["name"], row["irow"]))
        res.setdefault(metabs, dict())
        for suff in ("S", "D-", "D+", "DD", "T"):
            # get val and dev for this type of peak
            val=row.get("VALUE_"+suff,"")
            if (not val or val=="-"):
                continue
            dev=row.get("DEVIATION_"+suff,"") or row.get("DEVIATION_S")
            if suff in ("DD", "T"):
               dev=row.get("DEVIATION_DD/T","") or row.get("DEVIATION_S")
            if not dev:
                raise Exception("The field DEVIATION_%s is empty (%s: %s)"%(suff, ftbl["name"], row["irow"]))
            try:
                val=float(eval_expr(val))
            except ValueError:
                val=NaN
            try:
                dev=float(eval_expr(dev))
            except ValueError:
                continue
            # test validity
            if not dev:
                raise Exception("Deviation is not valid for VALUE_"+suff+" (%s: %s)")%(ftbl["name"], row["irow"])
            c_no=int(row["PEAK_NO"])
            if c_no > clen0:
                raise Exception("Carbon number "+str(c_no)+" is greater than carbon length "+str(clen0)+" for metabolite '"+metab0+"' (%s: %s)"%(ftbl["name"], row["irow"]))
            if suff == "D-" and c_no == 1:
                raise Exception("Peak D- cannot be set for metabolite "+metab0+", c_no=1 (%s: %s)"%(ftbl["name"], row["irow"]))
            if suff == "D+" and c_no == clen:
                raise Exception("Peak D+ cannot be set for metabolite "+metab+", c_no="+str(c_no)+" (%s: %s)"%(ftbl["name"], row["irow"]))
            if (suff == "DD" or suff == "T") and (c_no == 1 or c_no == clen or clen < 3):
                raise Exception("Peak DD (or T) cannot be set for metabolite "+metab+", c_no="+str(c_no)+", len="+str(clen)+" (%s: %s)"%(ftbl["name"], row["irow"]))
            res[metabs].setdefault(row["irow"], dict())
            res[metabs][row["irow"]][suff]={
               "val": val,
               "dev": dev,
               "id": ":".join(["p", metabs, row["PEAK_NO"], suff, str(row["irow"])]),
               "irow": str(row["irow"]),
               "c_no": c_no,
               "pooled":metabl,
            }
def proc_mass_meas(ftbl, netan):
    """Proceed PEAK_MEASUREMENT section of ftbl file, add the result to a list of dicts
    """
    ili=len(netan["mass_meas"])
    netan["mass_meas"]+=[{}]
    res=netan["mass_meas"][ili]
    # mass measurements
    # [ili]][metab][frag_mask][weight]={val:x, dev:y}
    metabs=""
    for row in ftbl.get("MASS_SPECTROMETRY",[]):
        #print row;##
        metabs=row["META_NAME"] or metabs
        # metabs can be metab1[+metab2[+...]]
        metabl=[v.strip() for v in metabs.split("+")]
        if (len(metabl) > 1):
            # pooling metabolites will need their concentraions
            mdif=oset(metabl).difference(netan["met_pools"])
            if mdif:
                raise Exception("Pooled metabolite(s) '%s' are absent in METABOLITE_POOLS section (%s: %s)"%(join(", ", mdif), ftbl["name"], row["irow"]))

        # check that all metabs are unique
        count=dict((i, metabl.count(i)) for i in oset(metabl))
        #print(count)
        notuniq=oset((i for i in count if count[i] > 1))
        if notuniq:
            raise Exception("Metabolite(s) '%s' is present twice or more (%s: %s)"%(join(", ", notuniq), ftbl["name"], row["irow"]))

        metab0=metabl[0] if metabl else ""
        if not metab0 in netan["metabs"]:
            raise Exception("Unknown metabolite name '%s' in MASS_MEASUREMENTS (%s: %s)"%(metab0, ftbl["name"], row["irow"]))
        clen0=netan["Clen"][metab0] if metab0 else 0
        if row["META_NAME"]:
            irow=int(row["irow"])
            frag=""
            #frag=",".join(str(i+1) for i in range(clen0))
            mask=(1<<clen0)-1
        frag=row["FRAGMENT"] or frag
        for metab in metabl:
            clen=netan["Clen"][metab]
            # test the validity
            if not metab in netan["metabs"]:
                raise Exception("Unknown metabolite name '%s' in MASS_SPECTROMETRY (%s: %s)."%(metab, ftbl["name"], row["irow"]))
            if metab in netan["output"] or metab in netan["input"]:
                raise Exception("""Measured metabolites have to be internal to network.
In case of output, you can add a fictitious metabolite following to '"""+metab+"' (seen in MASS_MEASUREMENTS).")
            clen=netan["Clen"][metab]
            if clen!=clen0 :
                raise Exception("Carbon length of '%s' (%d) is different from the length of '%s' (%d) (%s: %s)"%(metab, clen, metab0, clen0, ftbl["name"], row["irow"]))
        if row["FRAGMENT"]:
            # recalculate fragment mask
            mask=0
            for item in frag.split(","):
                try:
                    i=int(item)
                    if i > clen0:
                        raise Exception("An item '"+item+"' of carbon fragment is higher than metabolite '"+metab0+"' length "+str(clen0)+" (%s: %d)"%(ftbl["name"], irow))
                    # add this simple item to the mask
                    mask|=1<<(clen0-i)
                except ValueError:
                    # try the interval
                    try:
                        (start,end)=item.split("~")
                        #print "start,end=%s,%s" % (start,end);##
                        try:
                            for i in range(int(start),int(end)+1):
                                if i > clen0:
                                    raise ValueError("End of interval '"+item+"' is higher than metabolite '"+metab0+"' length "+str(clen0)+" (%s: %d)"%(ftbl["name"], irow))
                                mask|=1<<(clen0-i)
                        except ValueError:
                            raise ValueError("Badly formed fragment interval '"+item+"' (%s: %d)"%(ftbl["name"], irow))
                    except ValueError:
                        raise ValueError("Badly formed fragment interval '"+item+"' (%s: %d)"%(ftbl["name"], irow))
        m_id=":".join(["m", metabs, frag, str(irow)])
        weight=int(row["WEIGHT"])
        if sumbit(mask) < weight:
            raise Exception("Weight "+str(weight)+" is higher than fragment length "+frag+" (%s: %d)"%(ftbl["name"], irow))
        if clen < sumbit(mask):
            raise Exception("Fragment "+frag+" is longer than metabolite length "+str(clen)+" (%s: %d)"%(ftbl["name"], irow))
        res.setdefault(m_id, dict())
        res[m_id].setdefault(mask, dict())
        if not row["VALUE"]:
            raise Exception("The field VALUE is empty (%s: %s)"%(ftbl["name"], row["irow"]))
        if not row["DEVIATION"]:
            raise Exception("The field DEVIATION is empty (%s: %s)"%(ftbl["name"], row["irow"]))
        try:
            val=float(eval_expr(row["VALUE"]))
        except ValueError:
            val=NaN
        try:
            sdev=float(eval_expr(row["DEVIATION"]))
        except ValueError:
            raise ValueError("DEVIATION must evaluate to a real positive number (%s: %s)."%(ftbl["name"], row["irow"]))
        if sdev <= 0.:
            raise ValueError("DEVIATION must be positive (%s: %s)."%(ftbl["name"], row["irow"]))
        res[m_id][mask][weight]={
                "val":val,
                "dev":sdev,
                "id":":".join(["m", metabs, frag, row["WEIGHT"], str(row["irow"])]),
                "irow":str(row["irow"]),
                "pooled":metabl,
        }

def proc_kinopt(ftbl, netan):
    """Proceed label kinetics options from OPTIONS section: file_labcin, dt, tmax, nsubdiv_dt, funlab
    """
    # default values (used when absent in ftbl)
    de={"file_labcin": "", "dt": 1, "tmax": float("inf"), "nsubdiv_dt": 1, "funlabR": ""}
    if "opt" not in netan:
        netan["opt"]=dict((k, []) for k in de)
    # get ftbl OPTIONS -> d
    d=dict()
    for row in ftbl.get("OPTIONS",[]):
        if row["OPT_NAME"][:7] == "funlab:":
            d[row["OPT_NAME"]]=row["OPT_VALUE"]
        else:
            val=eval_expr(row["OPT_VALUE"], werr=None)
            d[row["OPT_NAME"]]=row["OPT_VALUE"] if val is None else val
    for k in de:
        if k not in netan["opt"]:
            netan["opt"][k]=[]
        elif type(netan["opt"][k]) != list:
            netan["opt"][k]=[] # if already set to a value, reset to a list
        ili=len(netan["opt"][k])
        netan["opt"][k]+=[{}]
        res=netan["opt"][k]
        res[ili]=d.get(k, de[k])
    # append funval entries
    if "funlab" not in netan["opt"]:
        netan["opt"]["funlab"]=[]
    ili=len(netan["opt"]["funlab"])
    netan["opt"]["funlab"]+=[{}]
    netan["opt"]["funlab"][ili]=dict((k[7:],v) for k,v in d.items() if k[:7] == "funlab:")

def mkfunlabli(d):
    "transform 'd' dict to a string representing a body of an R function calculating labeling dependent on time 't'"
    # d is like {'Gluc_1:{32: '{T=0; if (t >= T) 1 else NA}'}}
    return "%(li)s"%{"li": "list("+", ".join("`%(m)s`=list(%(i2f)s)"%{"m": m, "i2f": ", ".join(f"`{iso}`='{f}'" for iso,f in idi.items())} for m,idi in d.items())+")"}
    
