Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON model import and export #469

Merged
merged 12 commits into from Nov 13, 2019
4 changes: 2 additions & 2 deletions pysb/annotation.py
Expand Up @@ -22,12 +22,12 @@ class Annotation(object):

"""

def __init__(self, subject, object_, predicate="is"):
def __init__(self, subject, object_, predicate="is", _export=True):
self.subject = subject
self.object = object_
self.predicate = predicate
# if SelfExporter is in use, add the annotation to the model
if SelfExporter.do_export:
if SelfExporter.do_export and _export:
SelfExporter.default_model.add_annotation(self)

def __repr__(self):
Expand Down
10 changes: 9 additions & 1 deletion pysb/core.py
Expand Up @@ -1271,8 +1271,16 @@ def __getnewargs__(self):
return (self.name, self.value, False)

def __init__(self, name, value=0.0, _export=True):
self.value = float(value)
self.value = value
Component.__init__(self, name, _export)

@property
def value(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this new getter/setter for? I'm guessing round-trip testing exposed a code path where the normalization of .value to a float was getting bypassed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in the earm_1_3.py model where .value is used with integers, but these values were getting converted to floats after round-tripped to JSON.

return self._value

@value.setter
def value(self, new_value):
self._value = float(new_value)

def get_value(self):
return self.value
Expand Down
2 changes: 1 addition & 1 deletion pysb/examples/bngwiki_egfr_simple.py
Expand Up @@ -39,7 +39,7 @@


Monomer('EGF', ['R'])
Monomer('EGFR', ['L','CR1','Y1068'], {'Y1068':('U','P')})
Monomer('EGFR', ['L','CR1','Y1068'], {'Y1068': ['U', 'P']})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Monomer should just normalize sites and site_states.values() to list, but that can be done outside of this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we should normalize in core.py.

Monomer('Grb2', ['SH2','SH3'])
Monomer('Sos1', ['PxxP'])

Expand Down
4 changes: 4 additions & 0 deletions pysb/export/__init__.py
Expand Up @@ -17,13 +17,15 @@

- ``bngl``
- ``bng_net``
- ``json``
- ``kappa``
- ``potterswheel``
- ``sbml``
- ``python``
- ``pysb_flat``
- ``mathematica``
- ``matlab``
- ``stochkit``

In all cases, the exported model code will be printed to standard
out, allowing it to be inspected or redirected to another file.
Expand Down Expand Up @@ -71,6 +73,7 @@
python
pysb_flat
stochkit
json
"""

import re
Expand Down Expand Up @@ -123,6 +126,7 @@ def export(self):
formats = {
'bngl': 'BnglExporter',
'bng_net': 'BngNetExporter',
'json': 'JsonExporter',
'kappa': 'KappaExporter',
'potterswheel': 'PottersWheelExporter',
'sbml': 'SbmlExporter',
Expand Down
254 changes: 254 additions & 0 deletions pysb/export/json.py
@@ -0,0 +1,254 @@
"""
Module containing a class for exporting a PySB model to JSON

For information on how to use the model exporters, see the documentation
for :py:mod:`pysb.export`.
"""

from __future__ import absolute_import
from pysb.export import Exporter
from pysb.bng import generate_equations
import json
from pysb.core import Model, MultiState, KeywordMeta, Parameter, Expression


class JsonExporter(Exporter):
"""A class for returning the JSON for a given PySB model.

Inherits from :py:class:`pysb.export.Exporter`, which implements
basic functionality for all exporters.
"""

def export(self, include_netgen=False):
"""Generate the corresponding JSON for the PySB model associated
with the exporter.

Parameters
----------
include_netgen: bool
Include cached network generation data (reactions, species,
local function-derived parameters and expressions) if True.

Returns
-------
string
The JSON output for the model.
"""
return json.dumps(self.model, cls=PySBJSONWithNetworkEncoder
if include_netgen else PySBJSONEncoder)


class PySBJSONEncoder(json.JSONEncoder):
"""
Encode a PySB model in JSON

This encoder stores the model without caching the reaction network. To also
store the reaction network, see :py:class:`PySBJSONWithNetworkEncoder`.

Attributes correspond to their PySB equivalents (monomers, parameters, etc.)
and are mostly stored verbatim, with the following exceptions.

* MultiStates and the ANY and WILD state values use a special object format
* References to other components are stored using the component name
* Sympy expressions are encoded as strings using the default encoder

The protocol number (currently: 1) specifies semantic model compatibility,
and should be incremented if new features are added which affect how a model
is simulated or prevent a model being loaded by
:py:func:`pysb.importers.json.PySBJSONDecoder`.
"""
PROTOCOL = 1

@classmethod
def encode_keyword(cls, keyword):
return {'__object__': str(keyword)}

@classmethod
def encode_multistate(cls, stateval):
return {
'__object__': '__multistate__',
'sites': stateval.sites
}

@classmethod
def encode_monomer(cls, mon):
return {
'name': mon.name,
'sites': mon.sites,
'states': mon.site_states
}

@classmethod
def encode_compartment(cls, cpt):
return {
'name': cpt.name,
'parent': cpt.parent.name if cpt.parent else None,
'dimension': cpt.dimension,
'size': cpt.size.name if cpt.size else None
}

@classmethod
def encode_parameter(cls, par):
return {
'name': par.name,
'value': par.value
}

@classmethod
def encode_expression(cls, expr):
return {
'name': expr.name,
'expr': str(expr.expr)
}

@classmethod
def encode_monomer_pattern(cls, mp):
return {
'monomer': mp.monomer.name,
'site_conditions': mp.site_conditions,
'compartment': mp.compartment.name if mp.compartment else None,
'tag': mp._tag.name if mp._tag else None
}

@classmethod
def encode_complex_pattern(cls, cp):
return {
'monomer_patterns': [cls.encode_monomer_pattern(mp)
for mp in cp.monomer_patterns],
'compartment': cp.name if cp.compartment else None,
'match_once': cp.match_once,
'tag': cp._tag.name if cp._tag else None
}

@classmethod
def encode_reaction_pattern(cls, rp):
return {
'complex_patterns': [cls.encode_complex_pattern(cp)
for cp in rp.complex_patterns]
}

@classmethod
def encode_rule_expression(cls, rexp):
return {
'reactant_pattern': cls.encode_reaction_pattern(
rexp.reactant_pattern),
'product_pattern': cls.encode_reaction_pattern(
rexp.product_pattern),
'reversible': rexp.is_reversible
}

@classmethod
def encode_rule(cls, r):
return {
'name': r.name,
'rule_expression': cls.encode_rule_expression(r.rule_expression),
'rate_forward': r.rate_forward.name,
'rate_reverse': r.rate_reverse.name if r.rate_reverse else None,
'delete_molecules': r.delete_molecules,
'move_connected': r.move_connected
}

@classmethod
def encode_observable(cls, obs):
return {
'name': obs.name,
'reaction_pattern': cls.encode_reaction_pattern(
obs.reaction_pattern),
'match': obs.match
}

@classmethod
def encode_initial(cls, init):
return {
'pattern': cls.encode_complex_pattern(init.pattern),
'parameter_or_expression': init.value.name,
'fixed': init.fixed
}

@classmethod
def encode_annotation(cls, ann):
return {
'subject': 'model' if isinstance(ann.subject, Model)
else ann.subject.name,
'object': str(ann.object),
'predicate': str(ann.predicate)
}

@classmethod
def encode_tag(cls, tag):
return {
'name': tag.name
}

@classmethod
def encode_model(cls, model):
d = dict(protocol=cls.PROTOCOL, name=model.name)

encoders = {
'monomers': cls.encode_monomer,
'compartments': cls.encode_compartment,
'tags': cls.encode_tag,
'parameters': cls.encode_parameter,
'expressions': cls.encode_expression,
'rules': cls.encode_rule,
'observables': cls.encode_observable,
'initials': cls.encode_initial,
'annotations': cls.encode_annotation
}

for component_type, encoder in encoders.items():
d[component_type] = [encoder(component)
for component in
getattr(model, component_type)]

return d

def default(self, o):
if isinstance(o, Model):
return self.encode_model(o)
elif isinstance(o, MultiState):
return self.encode_multistate(o)
elif isinstance(o, KeywordMeta):
return self.encode_keyword(o)

return super(PySBJSONEncoder, self).default(o)


class PySBJSONWithNetworkEncoder(PySBJSONEncoder):
"""
Encode a PySB model and its reaction network in JSON

This encoder stores the model including the cached reaction network. To
encode the model without the reaction network, see
:py:class:`PySBJSONEncoder`, which also includes implementation details.
"""

@classmethod
def encode_reaction(cls, rxn):
rxn = rxn.copy()
rxn['rate'] = rxn['rate'].name \
if isinstance(rxn['rate'], (Parameter, Expression)) \
else str(rxn['rate'])
return rxn

@classmethod
def encode_model(cls, model):
d = super(PySBJSONWithNetworkEncoder, cls).encode_model(model)

# Ensure network generation has taken place
generate_equations(model)

additional_encoders = {
'_derived_parameters': cls.encode_parameter,
'_derived_expressions': cls.encode_expression,
'reactions': cls.encode_reaction,
'reactions_bidirectional': cls.encode_reaction,
'species': cls.encode_complex_pattern
}

for component_type, encoder in additional_encoders.items():
d[component_type] = [encoder(component)
for component in
getattr(model, component_type)]

return d