From b118f30f079d4481b507b00a6c1a6d1433e197f5 Mon Sep 17 00:00:00 2001 From: Ben Gyori Date: Wed, 4 Sep 2024 21:05:09 -0400 Subject: [PATCH 01/32] Test with Pydantic 2 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ef7f299aa..f76a117a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ license_files = [options] install_requires = - pydantic>=1.10,<2.0.0 + pydantic>=2.0.0 sympy typing_extensions networkx From 4c774eacec62243a21109baae04838ea597124e4 Mon Sep 17 00:00:00 2001 From: Ben Gyori Date: Wed, 4 Sep 2024 21:09:14 -0400 Subject: [PATCH 02/32] Update method for Pydantic v2 --- mira/metamodel/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mira/metamodel/utils.py b/mira/metamodel/utils.py index 8d98f1e75..06009f213 100644 --- a/mira/metamodel/utils.py +++ b/mira/metamodel/utils.py @@ -53,7 +53,7 @@ def validate(cls, v): return cls(v) @classmethod - def __modify_schema__(cls, field_schema): + def __get_pydantic_json_schema__(cls, field_schema): field_schema.update(type="string", example="2*x") def __str__(self): From 4e85486c99f9dfd56a2dc2182b5331af15629d3f Mon Sep 17 00:00:00 2001 From: Ben Gyori Date: Fri, 6 Sep 2024 18:48:51 -0400 Subject: [PATCH 03/32] Use migration tool to update code --- mira/dkg/api.py | 22 ++++---- mira/dkg/client.py | 52 ++++++++++--------- mira/dkg/grounding.py | 20 ++++---- mira/dkg/model.py | 44 ++++++++-------- mira/dkg/models.py | 4 +- mira/metamodel/comparison.py | 24 +++++---- mira/metamodel/template_model.py | 87 +++++++++++++++----------------- mira/metamodel/templates.py | 65 ++++++++++++------------ mira/metamodel/units.py | 17 ++++--- mira/metamodel/utils.py | 2 + mira/modeling/acsets/petri.py | 6 +-- mira/modeling/amr/petrinet.py | 26 +++++----- mira/modeling/amr/regnet.py | 18 +++---- 13 files changed, 194 insertions(+), 193 deletions(-) diff --git a/mira/dkg/api.py b/mira/dkg/api.py index 97b3c9ebd..34dc33289 100644 --- a/mira/dkg/api.py +++ b/mira/dkg/api.py @@ -25,18 +25,18 @@ class RelationQuery(BaseModel): """A query for relations in the domain knowledge graph.""" - source_type: Optional[str] = Field(description="The source type (i.e., prefix)", example="vo") + source_type: Optional[str] = Field(None, description="The source type (i.e., prefix)", examples=["vo"]) source_curie: Optional[str] = Field( - description="The source compact URI (CURIE)", example="doid:946" + None, description="The source compact URI (CURIE)", examples=["doid:946"] ) target_type: Optional[str] = Field( - description="The target type (i.e., prefix)", example="ncbitaxon" + None, description="The target type (i.e., prefix)", examples=["ncbitaxon"] ) target_curie: Optional[str] = Field( - description="The target compact URI (CURIE)", example="ncbitaxon:10090" + None, description="The target compact URI (CURIE)", examples=["ncbitaxon:10090"] ) relations: Union[None, str, List[str]] = Field( - description="A relation string or list of relation strings", example="vo:0001243" + None, description="A relation string or list of relation strings", examples=["vo:0001243"] ) relation_direction: Literal["right", "left", "both"] = Field( "right", description="The direction of the relationship" @@ -50,7 +50,7 @@ class RelationQuery(BaseModel): ge=0, ) limit: Optional[int] = Field( - description="A limit on the number of records returned", example=50, ge=0 + None, description="A limit on the number of records returned", examples=[50], ge=0 ) full: bool = Field( False, @@ -178,12 +178,12 @@ def get_transitive_closure( class RelationResponse(BaseModel): """A triple (or multi-predicate triple) with abbreviated data.""" - subject: str = Field(description="The CURIE of the subject of the triple", example="doid:96") + subject: str = Field(description="The CURIE of the subject of the triple", examples=["doid:96"]) predicate: Union[str, List[str]] = Field( description="A predicate or list of predicates as CURIEs", - example="ro:0002452", + examples=["ro:0002452"], ) - object: str = Field(description="The CURIE of the object of the triple", example="symp:0000001") + object: str = Field(description="The CURIE of the object of the triple", examples=["symp:0000001"]) class FullRelationResponse(BaseModel): @@ -418,10 +418,10 @@ class IsOntChildResult(BaseModel): """Result of a query to /is_ontological_child""" child_curie: str = Field(..., - example="vo:0001113", + examples=["vo:0001113"], description="The child CURIE") parent_curie: str = Field(..., - example="obi:0000047", + examples=["obi:0000047"], description="The parent CURIE") is_child: bool = Field( ..., diff --git a/mira/dkg/client.py b/mira/dkg/client.py index ee9b8a2c9..fcd0d40f4 100644 --- a/mira/dkg/client.py +++ b/mira/dkg/client.py @@ -44,28 +44,28 @@ class Relation(BaseModel): """A relationship between two entities in the DKG""" source_curie: str = Field( - description="The curie of the source node", example="probonto:k0000000" + description="The curie of the source node", examples=["probonto:k0000000"] ) target_curie: str = Field( - description="The curie of the target node", example="probonto:k0000007" + description="The curie of the target node", examples=["probonto:k0000007"] ) type: str = Field( - description="The type of the relation", example="has_parameter" + description="The type of the relation", examples=["has_parameter"] ) pred: str = Field( description="The curie of the relation type", - example="probonto:c0000062" + examples=["probonto:c0000062"] ) source: str = Field( - description="The prefix of the relation curie", example="probonto" + description="The prefix of the relation curie", examples=["probonto"] ) graph: str = Field( description="The URI of the relation", - example="https://raw.githubusercontent.com/probonto" - "/ontology/master/probonto4ols.owl" + examples=["https://raw.githubusercontent.com/probonto" + "/ontology/master/probonto4ols.owl"] ) version: str = Field( - description="The version number", example="2.5" + description="The version number", examples=["2.5"] ) @@ -73,43 +73,45 @@ class Entity(BaseModel): """An entity in the domain knowledge graph.""" id: str = Field( - ..., title="Compact URI", description="The CURIE of the entity", example="ido:0000511" + ..., title="Compact URI", description="The CURIE of the entity", examples=["ido:0000511"] ) - name: Optional[str] = Field(description="The name of the entity", example="infected population") - type: EntityType = Field(..., description="The type of the entity", example="class") - obsolete: bool = Field(..., description="Is the entity marked obsolete?", example=False) + name: Optional[str] = Field(None, description="The name of the entity", examples=["infected population"]) + type: EntityType = Field(..., description="The type of the entity", examples=["class"]) + obsolete: bool = Field(..., description="Is the entity marked obsolete?", examples=[False]) description: Optional[str] = Field( - description="The description of the entity.", - example="An organism population whose members have an infection.", + None, description="The description of the entity.", + examples=["An organism population whose members have an infection."], ) synonyms: List[Synonym] = Field( - default_factory=list, description="A list of string synonyms", example=[] + default_factory=list, description="A list of string synonyms", examples=[[]] ) alts: List[str] = Field( title="Alternative Identifiers", default_factory=list, - example=[], + examples=[[]], description="A list of alternative identifiers, given as CURIE strings.", ) xrefs: List[Xref] = Field( title="Database Cross-references", default_factory=list, - example=[], + examples=[[]], description="A list of database cross-references, given as CURIE strings.", ) labels: List[str] = Field( default_factory=list, - example=["ido"], + examples=[["ido"]], description="A list of Neo4j labels assigned to the entity.", ) properties: Dict[str, List[str]] = Field( default_factory=dict, description="A mapping of properties to their values", - example={}, + examples=[{}], ) # Gets auto-populated link: Optional[str] = None + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. @validator("link") def set_link(cls, value, values): """ @@ -235,12 +237,12 @@ class AskemEntity(Entity): """An extended entity with more ASKEM stuff loaded in.""" # TODO @ben please write descriptions for these - physical_min: Optional[float] = Field(description="") - physical_max: Optional[float] = Field(description="") - suggested_data_type: Optional[str] = Field(description="") - suggested_unit: Optional[str] = Field(description="") - typical_min: Optional[float] = Field(description="") - typical_max: Optional[float] = Field(description="") + physical_min: Optional[float] = Field(None, description="") + physical_max: Optional[float] = Field(None, description="") + suggested_data_type: Optional[str] = Field(None, description="") + suggested_unit: Optional[str] = Field(None, description="") + typical_min: Optional[float] = Field(None, description="") + typical_max: Optional[float] = Field(None, description="") class Neo4jClient: diff --git a/mira/dkg/grounding.py b/mira/dkg/grounding.py index eecc07b5a..e53c5da78 100644 --- a/mira/dkg/grounding.py +++ b/mira/dkg/grounding.py @@ -18,16 +18,16 @@ class GroundRequest(BaseModel): """A model representing the parameters to be passed to :func:`gilda.ground` for grounding.""" - text: str = Field(..., description="The text to be grounded", example="Infected Population") + text: str = Field(..., description="The text to be grounded", examples=["Infected Population"]) context: Optional[str] = Field( None, description="Context around the text to be grounded", - example="The infected population increased over the past month", + examples=["The infected population increased over the past month"], ) namespaces: Optional[List[str]] = Field( None, description="A list of namespaces to filter groundings to.", - example=["do", "mondo", "ido"], + examples=[["do", "mondo", "ido"]], ) @@ -37,30 +37,30 @@ class GroundResult(BaseModel): url: str = Field( ..., description="A URL that resolves the term to an external web service", - example=f"{BR_BASE}/ido:0000511", + examples=[f"{BR_BASE}/ido:0000511"], ) score: float = Field( - ..., description="The matching score calculated by Gilda", ge=0.0, le=1.0, example=0.78 + ..., description="The matching score calculated by Gilda", ge=0.0, le=1.0, examples=[0.78] ) prefix: str = Field( ..., description="The prefix corresponding to the ontology/database from which the term comes", - example="ido", + examples=["ido"], ) identifier: str = Field( ..., description="The local unique identifier for the term in the ontology/database denoted by the prefix", - example="0000511", + examples=["0000511"], ) curie: str = Field( ..., description="The compact URI that combines the prefix and local identifier.", - example="ido:0000511", + examples=["ido:0000511"], ) name: str = Field( - ..., description="The standard entity name for the term", example="infected population" + ..., description="The standard entity name for the term", examples=["infected population"] ) - status: str = Field(..., description="The match status, e.g., name, synonym", example="name") + status: str = Field(..., description="The match status, e.g., name, synonym", examples=["name"]) @classmethod def from_scored_match(cls, scored_match: ScoredMatch) -> "GroundResult": diff --git a/mira/dkg/model.py b/mira/dkg/model.py index a67e4ba2e..dba2ca06a 100644 --- a/mira/dkg/model.py +++ b/mira/dkg/model.py @@ -181,17 +181,17 @@ class StratificationQuery(BaseModel): template_model: Dict[str, Any] = Field( ..., description="The template model to stratify", - example=template_model_example + examples=[template_model_example] ) key: str = Field( ..., description="The (singular) name of the stratification", - example="city" + examples=["city"] ) strata: Set[str] = Field( ..., description="A list of the values for stratification", - example=["boston", "nyc"] + examples=[["boston", "nyc"]] ) strata_name_map: Union[Dict[str, str], None] = Field( None, @@ -199,9 +199,9 @@ class StratificationQuery(BaseModel): "renaming the concepts. If none given, will use the " "strata values as the names. This option only has an " "effect if ``modify_names`` is true.", - example={ + examples=[{ "geonames:4930956": "Boston", "geonames:5128581": "New York City" - }, + }], ) strata_name_lookup: bool = Field( False, @@ -209,26 +209,26 @@ class StratificationQuery(BaseModel): "strata values under the assumption that they are " "curies. This flag has no impact if ``strata_name_map`` " "is given.", - example=True + examples=[True] ) structure: Union[List[List[str]], None] = Field( None, description="An iterable of pairs corresponding to a directed network " "structure where each of the pairs has two strata. If none given, " "will assume a complete network structure.", - example=[["boston", "nyc"]], + examples=[[["boston", "nyc"]]], ) directed: bool = Field( False, description="Whether the model has directed edges or not.", - example=True + examples=[True] ) conversion_cls: Literal["natural_conversion", "controlled_conversion"] = Field( "natural_conversion", description="The template class to be used for conversions between " "strata defined by the network structure.", - example="natural_conversion", + examples=["natural_conversion"], ) cartesian_control: bool = Field( False, @@ -247,38 +247,38 @@ class StratificationQuery(BaseModel): through the perspective of the model) affect the infection of susceptible population in another city. """), - example=True + examples=[True] ) modify_names: bool = Field( True, description="If true, will modify the names of the concepts to " "include the strata (e.g., ``'S'`` becomes " "``'S_boston'``). If false, will keep the original names.", - example=True + examples=[True] ) params_to_stratify: Optional[List[str]] = Field( None, description="A list of parameters to stratify. If none given, " "will stratify all parameters.", - example=["beta"] + examples=[["beta"]] ) params_to_preserve: Optional[List[str]] = Field( None, description="A list of parameters to preserve. If none given, " "will stratify all parameters.", - example=["gamma"] + examples=[["gamma"]] ) concepts_to_stratify: Optional[List[str]] = Field( None, description="A list of concepts to stratify. If none given, " "will stratify all concepts.", - example=["susceptible", "infected"], + examples=[["susceptible", "infected"]], ) concepts_to_preserve: Optional[List[str]] = Field( None, description="A list of concepts to preserve. If none given, " "will stratify all concepts.", - example=["recovered"], + examples=[["recovered"]], ) def get_conversion_cls(self) -> Type[Template]: @@ -554,9 +554,9 @@ def model_to_graph_image( class TemplateModelDeltaQuery(BaseModel): - template_model1: Dict[str, Any] = Field(..., example=template_model_example) + template_model1: Dict[str, Any] = Field(..., examples=[template_model_example]) template_model2: Dict[str, Any] = Field( - ..., example=template_model_example_w_context + ..., examples=[template_model_example_w_context] ) @@ -629,7 +629,7 @@ def models_to_delta_image( class AddTranstitionQuery(BaseModel): template_model: Dict[str, Any] = Field( - ..., description="The template model to add the transition to", example=template_model_example + ..., description="The template model to add the transition to", examples=[template_model_example] ) subject_concept: Concept = Field(..., description="The subject concept") outcome_concept: Concept = Field(..., description="The outcome concept") @@ -661,9 +661,9 @@ def add_transition( class ModelComparisonQuery(BaseModel): template_models: List[Dict[str, Any]] = Field( - ..., example=[ + ..., examples=[[ template_model_example, template_model_example_w_context - ] + ]] ) @@ -700,7 +700,7 @@ def model_comparison( class AMRComparisonQuery(BaseModel): petrinet_models: List[Dict[str, Any]] = Field( - ..., example=[amr_petrinet_json, amr_petrinet_json_2_city] + ..., examples=[[amr_petrinet_json, amr_petrinet_json_2_city]] ) @@ -742,7 +742,7 @@ def askepetrinet_model_comparison( class FluxSpanQuery(BaseModel): model: Dict[str, Any] = Field( ..., - example=flux_span_query_example, + examples=[flux_span_query_example], description="The model to recover the ODE-semantics from.", ) diff --git a/mira/dkg/models.py b/mira/dkg/models.py index 58c632cc6..6a6f3e195 100644 --- a/mira/dkg/models.py +++ b/mira/dkg/models.py @@ -37,7 +37,7 @@ class Xref(BaseModel): id: str = Field(description="The CURIE of the cross reference") type: str = Field( description="The CURIE for the cross reference predicate", - example="skos:exactMatch", + examples=["skos:exactMatch"], ) @@ -47,5 +47,5 @@ class Synonym(BaseModel): value: str = Field(description="The text of the synonym") type: str = Field( description="The CURIE for the synonym predicate", - example="skos:exactMatch", + examples=["skos:exactMatch"], ) diff --git a/mira/metamodel/comparison.py b/mira/metamodel/comparison.py index a53c75931..d0bc32e7b 100644 --- a/mira/metamodel/comparison.py +++ b/mira/metamodel/comparison.py @@ -1,3 +1,6 @@ +from pydantic import Field, ConfigDict +from typing_extensions import Annotated + __all__ = ["ModelComparisonGraphdata", "TemplateModelComparison", "TemplateModelDelta", "RefinementClosure", "get_dkg_refinement_closure"] @@ -9,7 +12,7 @@ import networkx as nx import sympy -from pydantic import BaseModel, conint, Field +from pydantic import BaseModel, Field from tqdm import tqdm from .templates import Provenance, Concept, Template, SympyExprStr, IS_EQUAL, \ @@ -28,7 +31,7 @@ class DataNode(BaseModel): """A node in a ModelComparisonGraphdata""" node_type: Literal["template", "concept"] - model_id: conint(ge=0, strict=True) + model_id: Annotated[int, Field(ge=0, strict=True)] class TemplateNode(DataNode): @@ -69,15 +72,14 @@ class IntraModelEdge(DataEdge): class ModelComparisonGraphdata(BaseModel): """A data structure holding a graph representation of TemplateModel delta""" - class Config: - arbitrary_types_allowed = True - json_encoders = { - SympyExprStr: lambda e: str(e), - } - json_decoders = { - SympyExprStr: lambda e: safe_parse_expr(e), - Template: lambda t: Template.from_json(data=t), - } + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ + SympyExprStr: lambda e: str(e), + }, json_decoders={ + SympyExprStr: lambda e: safe_parse_expr(e), + Template: lambda t: Template.from_json(data=t), + }) template_models: Dict[int, TemplateModel] = Field( ..., description="A mapping of template model keys to template models" diff --git a/mira/metamodel/template_model.py b/mira/metamodel/template_model.py index 3ceb852b8..afe334366 100644 --- a/mira/metamodel/template_model.py +++ b/mira/metamodel/template_model.py @@ -1,3 +1,5 @@ +from pydantic import ConfigDict + __all__ = [ "Annotations", "TemplateModel", @@ -34,13 +36,11 @@ class Initial(BaseModel): expression: SympyExprStr = Field( description="The expression for the initial." ) - - class Config: - arbitrary_types_allowed = True - json_encoders = { - SympyExprStr: lambda e: str(e), - } - json_decoders = {SympyExprStr: lambda e: sympy.parse_expr(e)} + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ + SympyExprStr: lambda e: str(e), + }, json_decoders={SympyExprStr: lambda e: sympy.parse_expr(e)}) @classmethod def from_json(cls, data: Dict[str, Any], locals_dict=None) -> "Initial": @@ -135,13 +135,11 @@ class Observable(Concept): readout is not defined as a state variable but is rather a function of state variables. """ - - class Config: - arbitrary_types_allowed = True - json_encoders = { - SympyExprStr: lambda e: str(e), - } - json_decoders = {SympyExprStr: lambda e: safe_parse_expr(e)} + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ + SympyExprStr: lambda e: str(e), + }, json_decoders={SympyExprStr: lambda e: safe_parse_expr(e)}) expression: SympyExprStr = Field( description="The expression for the observable." @@ -185,7 +183,7 @@ class Time(BaseModel): name: str = Field( default="t", description="The symbol of the time variable in the model." ) - units: Optional[Unit] = Field(description="The units of the time variable.") + units: Optional[Unit] = Field(None, description="The units of the time variable.") class Author(BaseModel): @@ -203,8 +201,8 @@ class Annotations(BaseModel): """ name: Optional[str] = Field( - description="A human-readable label for the model", - example="SIR model of scenarios of COVID-19 spread in CA and NY", + None, description="A human-readable label for the model", + examples=["SIR model of scenarios of COVID-19 spread in CA and NY"], ) # identifiers: Dict[str, str] = Field( # description="Structured identifiers corresponding to the model artifact " @@ -217,8 +215,8 @@ class Annotations(BaseModel): # }, # ) description: Optional[str] = Field( - description="A description of the model", - example="The coronavirus disease 2019 (COVID-19) pandemic has placed " + None, description="A description of the model", + examples=["The coronavirus disease 2019 (COVID-19) pandemic has placed " "epidemic modeling at the forefront of worldwide public policy making. " "Nonetheless, modeling and forecasting the spread of COVID-19 remains a " "challenge. Here, we detail three regional scale models for forecasting " @@ -229,48 +227,48 @@ class Annotations(BaseModel): "time series data for a particular region. Capable of measuring and " "forecasting the impacts of social distancing, these models highlight the " "dangers of relaxing nonpharmaceutical public health interventions in the " - "absence of a vaccine or antiviral therapies.", + "absence of a vaccine or antiviral therapies."], ) license: Optional[str] = Field( - description="Information about the licensing of the model artifact. " + None, description="Information about the licensing of the model artifact. " "Ideally, given as an SPDX identifier like CC0 or CC-BY-4.0. For example, " "models from the BioModels databases are all licensed under the CC0 " "public attribution license.", - example="CC0", + examples=["CC0"], ) authors: List[Author] = Field( default_factory=list, description="A list of authors/creators of the model. This is not the same " "as the people who e.g., submitted the model to BioModels", - example=[ + examples=[[ Author(name="Andrea L Bertozzi"), Author(name="Elisa Franco"), Author(name="George Mohler"), Author(name="Martin B Short"), Author(name="Daniel Sledge"), - ], + ]], ) references: List[str] = Field( default_factory=list, description="A list of CURIEs (i.e., :) corresponding " "to literature references that describe the model. Do **not** duplicate the " "same publication with different CURIEs (e.g., using pubmed, pmc, and doi)", - example=["pubmed:32616574"], + examples=[["pubmed:32616574"]], ) # TODO agree on how we annotate this one, e.g. with a timedelta time_scale: Optional[str] = Field( - description="The granularity of the time element of the model, typically on " + None, description="The granularity of the time element of the model, typically on " "the scale of days, weeks, or months for epidemiology models", - example="day", + examples=["day"], ) time_start: Optional[datetime.datetime] = Field( - description="The start time of the applicability of a model, given as a datetime. " + None, description="The start time of the applicability of a model, given as a datetime. " "When the time scale is not so granular, leave the less granular fields as default, " "i.e., if the time scale is on months, give dates like YYYY-MM-01 00:00", # example=datetime.datetime(year=2020, month=3, day=1), ) time_end: Optional[datetime.datetime] = Field( - description="Similar to the start time of the applicability of a model, the end time " + None, description="Similar to the start time of the applicability of a model, the end time " "is given as a datetime. For example, the Bertozzi 2020 model is applicable between " "March and August 2020, so this field is annotated with August 1st, 2020.", # example=datetime.datetime(year=2020, month=8, day=1), @@ -282,10 +280,10 @@ class Annotations(BaseModel): "has multiple levels of granularity including city/state/country level terms. For example," "the Bertozzi 2020 model was for New York City (geonames:5128581) and California " "(geonames:5332921)", - example=[ + examples=[[ "geonames:5128581", "geonames:5332921", - ], + ]], ) pathogens: List[str] = Field( default_factory=list, @@ -294,9 +292,9 @@ class Annotations(BaseModel): "SARS-CoV-2, this is ncbitaxon:2697049. Do not confuse this field with terms for annotating " "the disease caused by the pathogen. Note that some models may have multiple pathogens, for " "simulating double pandemics such as the interaction with SARS-CoV-2 and the seasonal flu.", - example=[ + examples=[[ "ncbitaxon:2697049", - ], + ]], ) diseases: List[str] = Field( default_factory=list, @@ -304,9 +302,9 @@ class Annotations(BaseModel): "vocabulary for dieases, such as DOID, EFO, or MONDO. For example, the Bertozzi 2020 model " "is about SARS-CoV-2, which causes COVID-19. In the Human Disease Ontology (DOID), this " "is referenced by doid:0080600.", - example=[ + examples=[[ "doid:0080600", - ], + ]], ) hosts: List[str] = Field( default_factory=list, @@ -315,9 +313,9 @@ class Annotations(BaseModel): "human infection by SARS-CoV-2. Therefore, the appropriate annotation for this field " "would be ncbitaxon:9606. Note that some models have multiple hosts, such as Malaria " "models that consider humans and mosquitos.", - example=[ + examples=[[ "ncbitaxon:9606", - ], + ]], ) model_types: List[str] = Field( default_factory=list, @@ -326,10 +324,10 @@ class Annotations(BaseModel): " model', 'population model', etc. These should be annotated as CURIEs in the form " "of mamo:. For example, the Bertozzi 2020 model is a population " "model (mamo:0000028) and ordinary differential equation model (mamo:0000046)", - example=[ + examples=[[ "mamo:0000028", "mamo:0000046", - ], + ]], ) @@ -367,12 +365,11 @@ class TemplateModel(BaseModel): description="A structure containing time-related annotations. " "Note that all annotations are optional.", ) - - class Config: - json_encoders = { - SympyExprStr: lambda e: str(e), - } - json_decoders = {SympyExprStr: lambda e: safe_parse_expr(e)} + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict(json_encoders={ + SympyExprStr: lambda e: str(e), + }, json_decoders={SympyExprStr: lambda e: safe_parse_expr(e)}) def get_parameters_from_rate_law(self, rate_law) -> Set[str]: """Given a rate law, find its elements that are model parameters. diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index 84f4580d9..8d88d423f 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -3,6 +3,9 @@ Regenerate the JSON schema by running ``python -m mira.metamodel.schema``. """ +from pydantic import ConfigDict +from typing import Literal + __all__ = [ "Concept", "Template", @@ -125,15 +128,13 @@ class Concept(BaseModel): None, description="The units of the concept." ) _base_name: str = pydantic.PrivateAttr(None) - - class Config: - arbitrary_types_allowed = True - json_encoders = { - SympyExprStr: lambda e: str(e), - } - json_decoders = { - SympyExprStr: lambda e: sympy.parse_expr(e) - } + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ + SympyExprStr: lambda e: str(e), + }, json_decoders={ + SympyExprStr: lambda e: sympy.parse_expr(e) + }) def with_context(self, do_rename=False, curie_to_name_map=None, inplace=False, **context) -> "Concept": @@ -387,15 +388,13 @@ def from_json(cls, data) -> "Concept": class Template(BaseModel): """The Template is a parent class for model processes""" - - class Config: - arbitrary_types_allowed = True - json_encoders = { - SympyExprStr: lambda e: str(e), - } - json_decoders = { - SympyExprStr: lambda e: safe_parse_expr(e) - } + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ + SympyExprStr: lambda e: str(e), + }, json_decoders={ + SympyExprStr: lambda e: safe_parse_expr(e) + }) rate_law: Optional[SympyExprStr] = Field( default=None, description="The rate law for the template." @@ -890,7 +889,7 @@ class ControlledConversion(Template): """Specifies a process of controlled conversion from subject to outcome, controlled by the controller.""" - type: Literal["ControlledConversion"] = Field("ControlledConversion", const=True) + type: Literal["ControlledConversion"] = "ControlledConversion" controller: Concept = Field(..., description="The controller of the conversion.") subject: Concept = Field(..., description="The subject of the conversion.") outcome: Concept = Field(..., description="The outcome of the conversion.") @@ -976,7 +975,7 @@ def get_key(self, config: Optional[Config] = None): class GroupedControlledConversion(Template): - type: Literal["GroupedControlledConversion"] = Field("GroupedControlledConversion", const=True) + type: Literal["GroupedControlledConversion"] = "GroupedControlledConversion" controllers: List[Concept] = Field(..., description="The controllers of the conversion.") subject: Concept = Field(..., description="The subject of the conversion.") outcome: Concept = Field(..., description="The outcome of the conversion.") @@ -1066,7 +1065,7 @@ def add_controller(self, controller: Concept) -> "GroupedControlledConversion": class GroupedControlledProduction(Template): """Specifies a process of production controlled by several controllers""" - type: Literal["GroupedControlledProduction"] = Field("GroupedControlledProduction", const=True) + type: Literal["GroupedControlledProduction"] = "GroupedControlledProduction" controllers: List[Concept] = Field(..., description="The controllers of the production.") outcome: Concept = Field(..., description="The outcome of the production.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the production.") @@ -1152,9 +1151,7 @@ def with_context( class ControlledProduction(Template): """Specifies a process of production controlled by one controller""" - type: Literal["ControlledProduction"] = Field( - "ControlledProduction", const=True - ) + type: Literal["ControlledProduction"] = "ControlledProduction" controller: Concept = Field( ..., description="The controller of the production." ) @@ -1240,7 +1237,7 @@ def with_context( class NaturalConversion(Template): """Specifies a process of natural conversion from subject to outcome""" - type: Literal["NaturalConversion"] = Field("NaturalConversion", const=True) + type: Literal["NaturalConversion"] = "NaturalConversion" subject: Concept = Field(..., description="The subject of the conversion.") outcome: Concept = Field(..., description="The outcome of the conversion.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the conversion.") @@ -1278,7 +1275,7 @@ def get_key(self, config: Optional[Config] = None): class MultiConversion(Template): """Specifies a conversion process of multiple subjects and outcomes.""" - type: Literal["MultiConversion"] = Field("MultiConversion", const=True) + type: Literal["MultiConversion"] = "MultiConversion" subjects: List[Concept] = Field(..., description="The subjects of the conversion.") outcomes: List[Concept] = Field(..., description="The outcomes of the conversion.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the conversion.") @@ -1325,7 +1322,7 @@ def with_context( class ReversibleFlux(Template): """Specifies a reversible flux between a left and right side.""" - type: Literal["ReversibleFlux"] = Field("ReversibleFlux", const=True) + type: Literal["ReversibleFlux"] = "ReversibleFlux" left: List[Concept] = Field(..., description="The left hand side of the flux.") right: List[Concept] = Field(..., description="The right hand side of the flux.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the flux.") @@ -1372,7 +1369,7 @@ def with_context( class NaturalProduction(Template): """A template for the production of a species at a constant rate.""" - type: Literal["NaturalProduction"] = Field("NaturalProduction", const=True) + type: Literal["NaturalProduction"] = "NaturalProduction" outcome: Concept = Field(..., description="The outcome of the production.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the production.") @@ -1406,7 +1403,7 @@ def with_context( class NaturalDegradation(Template): """A template for the degradataion of a species at a proportional rate to its amount.""" - type: Literal["NaturalDegradation"] = Field("NaturalDegradation", const=True) + type: Literal["NaturalDegradation"] = "NaturalDegradation" subject: Concept = Field(..., description="The subject of the degradation.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the degradation.") @@ -1439,7 +1436,7 @@ def with_context( class ControlledDegradation(Template): """Specifies a process of degradation controlled by one controller""" - type: Literal["ControlledDegradation"] = Field("ControlledDegradation", const=True) + type: Literal["ControlledDegradation"] = "ControlledDegradation" controller: Concept = Field(..., description="The controller of the degradation.") subject: Concept = Field(..., description="The subject of the degradation.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the degradation.") @@ -1518,7 +1515,7 @@ def with_context( class GroupedControlledDegradation(Template): """Specifies a process of degradation controlled by several controllers""" - type: Literal["GroupedControlledDegradation"] = Field("GroupedControlledDegradation", const=True) + type: Literal["GroupedControlledDegradation"] = "GroupedControlledDegradation" controllers: List[Concept] = Field(..., description="The controllers of the degradation.") subject: Concept = Field(..., description="The subject of the degradation.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the degradation.") @@ -1602,7 +1599,7 @@ def with_context( class NaturalReplication(Template): """Specifies a process of natural replication of a subject.""" - type: Literal["NaturalReplication"] = Field("NaturalReplication", const=True) + type: Literal["NaturalReplication"] = "NaturalReplication" subject: Concept = Field(..., description="The subject of the replication.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the template.") @@ -1635,7 +1632,7 @@ def get_key(self, config: Optional[Config] = None): class ControlledReplication(Template): """Specifies a process of replication controlled by one controller""" - type: Literal["ControlledReplication"] = Field("ControlledReplication", const=True) + type: Literal["ControlledReplication"] = "ControlledReplication" controller: Concept = Field(..., description="The controller of the replication.") subject: Concept = Field(..., description="The subject of the replication.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance of the replication.") @@ -1695,7 +1692,7 @@ def with_context( class StaticConcept(Template): """Specifies a standalone Concept that is not part of a process.""" - type: Literal["StaticConcept"] = Field("StaticConcept", const=True) + type: Literal["StaticConcept"] = "StaticConcept" subject: Concept = Field(..., description="The subject.") provenance: List[Provenance] = Field(default_factory=list, description="The provenance.") concept_keys: ClassVar[List[str]] = ["subject"] diff --git a/mira/metamodel/units.py b/mira/metamodel/units.py index 3de22139d..34b949423 100644 --- a/mira/metamodel/units.py +++ b/mira/metamodel/units.py @@ -1,3 +1,5 @@ +from pydantic import ConfigDict + __all__ = [ 'Unit', 'person_units', @@ -32,14 +34,13 @@ def load_units(): class Unit(BaseModel): """A unit of measurement.""" - class Config: - arbitrary_types_allowed = True - json_encoders = { - SympyExprStr: lambda e: str(e), - } - json_decoders = { - SympyExprStr: lambda e: sympy.parse_expr(e) - } + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ + SympyExprStr: lambda e: str(e), + }, json_decoders={ + SympyExprStr: lambda e: sympy.parse_expr(e) + }) expression: SympyExprStr = Field( description="The expression for the unit." diff --git a/mira/metamodel/utils.py b/mira/metamodel/utils.py index 06009f213..da6d924a1 100644 --- a/mira/metamodel/utils.py +++ b/mira/metamodel/utils.py @@ -39,6 +39,8 @@ def safe_parse_expr(s: str, local_dict=None) -> sympy.Expr: class SympyExprStr(sympy.Expr): @classmethod + # TODO[pydantic]: We couldn't refactor `__get_validators__`, please create the `__get_pydantic_core_schema__` manually. + # Check https://docs.pydantic.dev/latest/migration/#defining-custom-types for more information. def __get_validators__(cls): yield cls.validate diff --git a/mira/modeling/acsets/petri.py b/mira/modeling/acsets/petri.py index 07743c634..b18af6b5a 100644 --- a/mira/modeling/acsets/petri.py +++ b/mira/modeling/acsets/petri.py @@ -21,7 +21,7 @@ class State(BaseModel): sname: str - sprop: Optional[Dict] + sprop: Optional[Dict] = None #mira_ids: str #mira_context: str #mira_initial_value: Optional[float] @@ -29,8 +29,8 @@ class State(BaseModel): class Transition(BaseModel): tname: str - rate: Optional[float] - tprop: Optional[Dict] + rate: Optional[float] = None + tprop: Optional[Dict] = None #template_type: str #parameter_name: Optional[str] #parameter_value: Optional[str] diff --git a/mira/modeling/amr/petrinet.py b/mira/modeling/amr/petrinet.py index 55244e84e..c3b491301 100644 --- a/mira/modeling/amr/petrinet.py +++ b/mira/modeling/amr/petrinet.py @@ -381,8 +381,8 @@ class Initial(BaseModel): class TransitionProperties(BaseModel): - name: Optional[str] - grounding: Optional[Dict] + name: Optional[str] = None + grounding: Optional[Dict] = None class Rate(BaseModel): @@ -404,7 +404,7 @@ class Units(BaseModel): class State(BaseModel): id: str name: Optional[str] = None - grounding: Optional[Dict] + grounding: Optional[Dict] = None units: Optional[Units] = None @@ -412,15 +412,15 @@ class Transition(BaseModel): id: str input: List[str] output: List[str] - grounding: Optional[Dict] - properties: Optional[TransitionProperties] + grounding: Optional[Dict] = None + properties: Optional[TransitionProperties] = None class Parameter(BaseModel): id: str description: Optional[str] = None value: Optional[float] = None - grounding: Optional[Dict] + grounding: Optional[Dict] = None distribution: Optional[Distribution] = None units: Optional[Units] = None @@ -438,8 +438,8 @@ class Time(BaseModel): class Observable(BaseModel): id: str - name: Optional[str] - grounding: Optional[Dict] + name: Optional[str] = None + grounding: Optional[Dict] = None expression: str expression_mathml: str @@ -453,12 +453,12 @@ class OdeSemantics(BaseModel): rates: List[Rate] initials: List[Initial] parameters: List[Parameter] - time: Optional[Time] + time: Optional[Time] = None observables: List[Observable] class Ode(BaseModel): - ode: Optional[OdeSemantics] + ode: Optional[OdeSemantics] = None class Header(BaseModel): @@ -472,7 +472,7 @@ class Header(BaseModel): class ModelSpecification(BaseModel): """A Pydantic model corresponding to the PetriNet JSON schema.""" header: Header - properties: Optional[Dict] + properties: Optional[Dict] = None model: PetriModel - semantics: Optional[Ode] - metadata: Optional[Dict] + semantics: Optional[Ode] = None + metadata: Optional[Dict] = None diff --git a/mira/modeling/amr/regnet.py b/mira/modeling/amr/regnet.py index bf128d0eb..62b3dec14 100644 --- a/mira/modeling/amr/regnet.py +++ b/mira/modeling/amr/regnet.py @@ -460,9 +460,9 @@ class Initial(BaseModel): class TransitionProperties(BaseModel): - name: Optional[str] - grounding: Optional[Dict] - rate: Optional[Dict] + name: Optional[str] = None + grounding: Optional[Dict] = None + rate: Optional[Dict] = None class Rate(BaseModel): @@ -485,7 +485,7 @@ class Transition(BaseModel): id: str input: List[str] output: List[str] - properties: Optional[TransitionProperties] + properties: Optional[TransitionProperties] = None class Parameter(BaseModel): @@ -517,18 +517,18 @@ class Header(BaseModel): class OdeSemantics(BaseModel): rates: List[Rate] - time: Optional[Time] + time: Optional[Time] = None observables: List[Observable] class Ode(BaseModel): - ode: Optional[OdeSemantics] + ode: Optional[OdeSemantics] = None class ModelSpecification(BaseModel): """A Pydantic model specification of the model.""" header: Header - properties: Optional[Dict] + properties: Optional[Dict] = None model: RegNetModel - semantics: Optional[Ode] - metadata: Optional[Dict] + semantics: Optional[Ode] = None + metadata: Optional[Dict] = None From 7e602888347b1390d8c283e7043e0c16ae031ce5 Mon Sep 17 00:00:00 2001 From: Ben Gyori Date: Fri, 6 Sep 2024 20:46:41 -0400 Subject: [PATCH 04/32] Address remaining issues manually --- mira/dkg/client.py | 6 ++---- mira/metamodel/utils.py | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mira/dkg/client.py b/mira/dkg/client.py index fcd0d40f4..1bef3ce4c 100644 --- a/mira/dkg/client.py +++ b/mira/dkg/client.py @@ -14,7 +14,7 @@ import pystow import requests from neo4j import GraphDatabase, Transaction, unit_of_work -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, field_validator from tqdm import tqdm from typing_extensions import Literal, TypeAlias @@ -110,9 +110,7 @@ class Entity(BaseModel): # Gets auto-populated link: Optional[str] = None - # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. - @validator("link") + @field_validator("link") def set_link(cls, value, values): """ Set the value of the ``link`` field based on the value of the ``id`` diff --git a/mira/metamodel/utils.py b/mira/metamodel/utils.py index da6d924a1..da42c808c 100644 --- a/mira/metamodel/utils.py +++ b/mira/metamodel/utils.py @@ -39,9 +39,7 @@ def safe_parse_expr(s: str, local_dict=None) -> sympy.Expr: class SympyExprStr(sympy.Expr): @classmethod - # TODO[pydantic]: We couldn't refactor `__get_validators__`, please create the `__get_pydantic_core_schema__` manually. - # Check https://docs.pydantic.dev/latest/migration/#defining-custom-types for more information. - def __get_validators__(cls): + def __get_pydantic_core_schema__(cls): yield cls.validate @classmethod From e78a9cc1377f94b9a6c8a3b0167658b696a5d6f6 Mon Sep 17 00:00:00 2001 From: Ben Gyori Date: Fri, 6 Sep 2024 21:10:31 -0400 Subject: [PATCH 05/32] Add additional manual fixes --- mira/metamodel/comparison.py | 2 ++ mira/metamodel/template_model.py | 2 ++ mira/metamodel/units.py | 20 +++++++++++++++----- mira/metamodel/utils.py | 16 +++++++++++++++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/mira/metamodel/comparison.py b/mira/metamodel/comparison.py index d0bc32e7b..a92acfcb4 100644 --- a/mira/metamodel/comparison.py +++ b/mira/metamodel/comparison.py @@ -30,6 +30,7 @@ class DataNode(BaseModel): """A node in a ModelComparisonGraphdata""" + model_config = ConfigDict(protected_namespaces=()) node_type: Literal["template", "concept"] model_id: Annotated[int, Field(ge=0, strict=True)] @@ -37,6 +38,7 @@ class DataNode(BaseModel): class TemplateNode(DataNode): """A node in a ModelComparisonGraphdata representing a Template""" + model_config = ConfigDict(protected_namespaces=()) type: str rate_law: Optional[SympyExprStr] = \ Field(default=None, description="The rate law of this template") diff --git a/mira/metamodel/template_model.py b/mira/metamodel/template_model.py index afe334366..1bd344d7b 100644 --- a/mira/metamodel/template_model.py +++ b/mira/metamodel/template_model.py @@ -200,6 +200,8 @@ class Annotations(BaseModel): a well-annotated SIR model in the BioModels database. """ + model_config = ConfigDict(protected_namespaces=()) + name: Optional[str] = Field( None, description="A human-readable label for the model", examples=["SIR model of scenarios of COVID-19 spread in CA and NY"], diff --git a/mira/metamodel/units.py b/mira/metamodel/units.py index 34b949423..ac1378290 100644 --- a/mira/metamodel/units.py +++ b/mira/metamodel/units.py @@ -36,11 +36,15 @@ class Unit(BaseModel): """A unit of measurement.""" # TODO[pydantic]: The following keys were removed: `json_encoders`. # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ - SympyExprStr: lambda e: str(e), - }, json_decoders={ - SympyExprStr: lambda e: sympy.parse_expr(e) - }) + model_config = ConfigDict( + arbitrary_types_allowed=True, + json_encoders={ + SympyExprStr: lambda e: str(e), + }, + #json_decoders={ + # SympyExprStr: lambda e: sympy.parse_expr(e) + #} + ) expression: SympyExprStr = Field( description="The expression for the unit." @@ -56,6 +60,12 @@ def from_json(cls, data: Dict[str, Any]) -> "Unit": ) return cls(**data) + @classmethod + def model_validate(cls, obj): + if isinstance(obj, dict) and 'expression' in obj: + obj['expression'] = SympyExprStr(obj['expression']) + return super().model_validate(obj) + person_units = Unit(expression=sympy.Symbol('person')) day_units = Unit(expression=sympy.Symbol('day')) diff --git a/mira/metamodel/utils.py b/mira/metamodel/utils.py index da42c808c..f14ed2df2 100644 --- a/mira/metamodel/utils.py +++ b/mira/metamodel/utils.py @@ -5,6 +5,10 @@ import re import unicodedata +from typing import Any +from pydantic import GetCoreSchemaHandler +from pydantic_core import core_schema + def get_parseable_expression(s: str) -> str: """Return an expression that can be parsed using sympy.""" @@ -39,9 +43,19 @@ def safe_parse_expr(s: str, local_dict=None) -> sympy.Expr: class SympyExprStr(sympy.Expr): @classmethod - def __get_pydantic_core_schema__(cls): + def __get_validators__(cls): yield cls.validate + @classmethod + def __get_pydantic_core_schema__( + cls, source_type: Any, handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + return core_schema.union_schema([ + core_schema.is_instance_schema(cls), + core_schema.no_info_plain_validator_function(cls.validate) + ]) + + @classmethod def validate(cls, v): if isinstance(v, cls): From bb7fe70a20c5c73016466d088a60f215564a8f14 Mon Sep 17 00:00:00 2001 From: Ben Gyori Date: Fri, 6 Sep 2024 21:23:20 -0400 Subject: [PATCH 06/32] Further manual fixes to get things to work --- mira/metamodel/schema.py | 41 +++++++++++++++++--------------- mira/metamodel/template_model.py | 4 ++-- mira/metamodel/utils.py | 11 +++++---- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/mira/metamodel/schema.py b/mira/metamodel/schema.py index 1a3dea2d1..a7a321738 100644 --- a/mira/metamodel/schema.py +++ b/mira/metamodel/schema.py @@ -6,7 +6,8 @@ from pathlib import Path import pydantic -from pydantic import BaseModel +from pydantic import BaseModel, create_model +from pydantic.json_schema import model_json_schema from . import Concept, Template, TemplateModel @@ -15,29 +16,31 @@ def get_json_schema(): - """Get the JSON schema for MIRA. - - Returns - ------- - : JSON - The JSON schema for MIRA. - """ + """Get the JSON schema for MIRA.""" rv = { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/indralab/mira/main/mira/metamodel/schema.json", + "title": "MIRA Metamodel Template Schema", + "description": "MIRA metamodel templates give a high-level abstraction of modeling appropriate for many domains.", } - rv.update( - pydantic.schema.schema( - [ - Concept, - Template, - *Template.__subclasses__(), - TemplateModel, - ], - title="MIRA Metamodel Template Schema", - description="MIRA metamodel templates give a high-level abstraction of modeling appropriate for many domains.", - ) + + models = [Concept, Template, *Template.__subclasses__(), TemplateModel] + + CombinedModel = create_model( + "CombinedModel", + **{model.__name__: (model, ...) for model in models} ) + + schema = model_json_schema( + CombinedModel, + mode='validation', + ) + + # Remove the top-level 'title' and 'description' from the generated schema + schema.pop('title', None) + schema.pop('description', None) + + rv.update(schema) return rv diff --git a/mira/metamodel/template_model.py b/mira/metamodel/template_model.py index 1bd344d7b..a5487cfd4 100644 --- a/mira/metamodel/template_model.py +++ b/mira/metamodel/template_model.py @@ -357,13 +357,13 @@ class TemplateModel(BaseModel): ) annotations: Optional[Annotations] = Field( - default_factory=None, + default=None, description="A structure containing model-level annotations. " "Note that all annotations are optional.", ) time: Optional[Time] = Field( - default_factory=None, + default=None, description="A structure containing time-related annotations. " "Note that all annotations are optional.", ) diff --git a/mira/metamodel/utils.py b/mira/metamodel/utils.py index f14ed2df2..e8306a07c 100644 --- a/mira/metamodel/utils.py +++ b/mira/metamodel/utils.py @@ -50,10 +50,10 @@ def __get_validators__(cls): def __get_pydantic_core_schema__( cls, source_type: Any, handler: GetCoreSchemaHandler ) -> core_schema.CoreSchema: - return core_schema.union_schema([ + return handler.resolve_ref_schema(core_schema.union_schema([ core_schema.is_instance_schema(cls), core_schema.no_info_plain_validator_function(cls.validate) - ]) + ])) @classmethod @@ -67,8 +67,11 @@ def validate(cls, v): return cls(v) @classmethod - def __get_pydantic_json_schema__(cls, field_schema): - field_schema.update(type="string", example="2*x") + def __get_pydantic_json_schema__(cls, core_schema, handler): + json_schema = handler(core_schema) + json_schema.update(type="string", format="sympy-expr") + return json_schema + #field_schema.update(type="string", example="2*x") def __str__(self): return super().__str__()[len(self.__class__.__name__)+1:-1] From c685f57bf10f26508a10a170901fdbeac92cd5ef Mon Sep 17 00:00:00 2001 From: Ben Gyori Date: Sat, 7 Sep 2024 15:21:40 -0400 Subject: [PATCH 07/32] Bump fastapi version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f76a117a2..1f05bf218 100644 --- a/setup.cfg +++ b/setup.cfg @@ -68,7 +68,7 @@ metaregistry = bioregistry[web] more_click web = - fastapi<0.87.0 + fastapi>=0.100.0 flask flasgger bootstrap-flask From 2414d47a68ba6bede6154adb4a9ce8b3c8bf3ce9 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 9 Sep 2024 10:28:12 -0400 Subject: [PATCH 08/32] Add optional tag to concept display_name and use default for parameter class --- mira/metamodel/template_model.py | 4 ++-- mira/metamodel/templates.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mira/metamodel/template_model.py b/mira/metamodel/template_model.py index a5487cfd4..485b82afd 100644 --- a/mira/metamodel/template_model.py +++ b/mira/metamodel/template_model.py @@ -119,11 +119,11 @@ class Parameter(Concept): """A Parameter is a special type of Concept that carries a value.""" value: Optional[float] = Field( - default_factory=None, description="Value of the parameter." + default=None, description="Value of the parameter." ) distribution: Optional[Distribution] = Field( - default_factory=None, + default=None, description="A distribution of values for the parameter.", ) diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index 8d88d423f..b07754aa9 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -112,7 +112,7 @@ class Concept(BaseModel): """ name: str = Field(..., description="The name of the concept.") - display_name: str = \ + display_name: Optional[str]= \ Field(None, description="An optional display name for the concept. " "If not provided, the name can be used for " "display purposes.") From 2ee121828db741487050f361ae358918e06e33db Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 9 Sep 2024 13:36:33 -0400 Subject: [PATCH 09/32] Convert transition names to strings for sbml qual models --- mira/sources/sbml/qual_processor.py | 35 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/mira/sources/sbml/qual_processor.py b/mira/sources/sbml/qual_processor.py index ed2124bf9..fb77545b2 100644 --- a/mira/sources/sbml/qual_processor.py +++ b/mira/sources/sbml/qual_processor.py @@ -66,7 +66,8 @@ def _lookup_concepts_filtered(species_ids) -> List[Concept]: for transition_id, transition in enumerate( self.qual_model_plugin.transitions ): - transition_name = transition.id + transition_name = str(transition.id) + transition_display_name = transition.id input_names = [ qual_species.qualitative_species @@ -108,16 +109,16 @@ def _lookup_concepts_filtered(species_ids) -> List[Concept]: templates.append( NaturalDegradation( subject=input_concepts[0], - name=transition_id, - display_name=transition_name, + name=transition_name, + display_name=transition_display_name, ) ) elif len(input_concepts) == 0 and len(output_concepts) == 1: templates.append( NaturalProduction( outcome=output_concepts[0], - name=transition_id, - display_name=transition_name, + name=transition_name, + display_name=transition_display_name, ) ) elif len(input_concepts) == 1 and len(output_concepts) == 1: @@ -125,8 +126,8 @@ def _lookup_concepts_filtered(species_ids) -> List[Concept]: NaturalConversion( subject=input_concepts[0], outcome=output_concepts[0], - name=transition_id, - display_name=transition_name, + name=transition_name, + display_name=transition_display_name, ) ) else: @@ -139,8 +140,8 @@ def _lookup_concepts_filtered(species_ids) -> List[Concept]: ControlledProduction( controller=positive_controller_concepts[0], outcome=output_concepts[0], - name=transition_id, - display_name=transition_name, + name=transition_name, + display_name=transition_display_name, ) ) else: @@ -148,8 +149,8 @@ def _lookup_concepts_filtered(species_ids) -> List[Concept]: GroupedControlledProduction( controllers=positive_controller_concepts, outcome=output_concepts[0], - name=transition_id, - display_name=transition_name, + name=transition_name, + display_name=transition_display_name, ) ) elif ( @@ -161,8 +162,8 @@ def _lookup_concepts_filtered(species_ids) -> List[Concept]: ControlledDegradation( controller=negative_controller_concepts[0], subject=input_concepts[0], - name=transition_id, - display_name=transition_name, + name=transition_name, + display_name=transition_display_name, ) ) else: @@ -170,8 +171,8 @@ def _lookup_concepts_filtered(species_ids) -> List[Concept]: GroupedControlledDegradation( controllers=negative_controller_concepts, subject=input_concepts[0], - name=transition_id, - display_name=transition_name, + name=transition_name, + display_name=transition_display_name, ) ) elif ( @@ -183,8 +184,8 @@ def _lookup_concepts_filtered(species_ids) -> List[Concept]: controllers=positive_controller_concepts + negative_controller_concepts, outcome=output_concepts[0], - name=transition_id, - display_name=transition_name, + name=transition_name, + display_name=transition_display_name, ) ) From 91c27173072e74e39109c841a1b874f8d2207f13 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 9 Sep 2024 13:58:46 -0400 Subject: [PATCH 10/32] add ModelComparisonGraphdata as accepted type --- mira/dkg/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mira/dkg/model.py b/mira/dkg/model.py index dba2ca06a..5d964a569 100644 --- a/mira/dkg/model.py +++ b/mira/dkg/model.py @@ -668,7 +668,7 @@ class ModelComparisonQuery(BaseModel): class ModelComparisonResponse(BaseModel): - graph_comparison_data: Dict[str, Any] #ModelComparisonGraphdata + graph_comparison_data: Union[Dict[str, Any], ModelComparisonGraphdata] #ModelComparisonGraphdata similarity_scores: List[Dict[str, Union[List[int], float]]] = Field( ..., description="A dictionary of similarity scores between all the " "provided models." From 5e20b8f2d6c8ce559b2233ec81de726882f3fa9e Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 9 Sep 2024 14:19:43 -0400 Subject: [PATCH 11/32] Convert flow names to strings --- mira/sources/acsets/stockflow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mira/sources/acsets/stockflow.py b/mira/sources/acsets/stockflow.py index 15b10602e..f274ed226 100644 --- a/mira/sources/acsets/stockflow.py +++ b/mira/sources/acsets/stockflow.py @@ -107,8 +107,9 @@ def template_model_from_stockflow_ascet_json(model_json) -> TemplateModel: outputs = [] # flow_id or flow_name for template name? - flow_id = flow["_id"] # required - flow_name = flow.get("fname") + flow_id = flow["_id"] # required + flow_name = str(flow_id) + flow_display_name = flow.get("fname") inputs.append(input) outputs.append(output) @@ -135,8 +136,8 @@ def template_model_from_stockflow_ascet_json(model_json) -> TemplateModel: output_concepts, controller_concepts, expression_sympy, - flow_id, flow_name, + flow_display_name, ) ) @@ -162,7 +163,7 @@ def stock_to_concept(stock) -> Concept: : The concept created from the stock """ - name = stock["_id"] + name = str(stock["_id"]) display_name = stock.get("sname") grounding = stock.get("grounding", {}) identifiers = grounding.get("identifiers", {}) From a54129514ff69d4a3214b2120b752f98f5ae98ee Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 9 Sep 2024 14:24:47 -0400 Subject: [PATCH 12/32] Allow int as values for concept context --- mira/metamodel/templates.py | 2 +- tests/test_dkg.py | 482 ++++++++++++++++++------------------ tests/test_graph_export.py | 258 +++++++++---------- 3 files changed, 371 insertions(+), 371 deletions(-) diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index b07754aa9..2895d865d 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -121,7 +121,7 @@ class Concept(BaseModel): identifiers: Mapping[str, str] = Field( default_factory=dict, description="A mapping of namespaces to identifiers." ) - context: Mapping[str, str] = Field( + context: Mapping[str, Union[str,int]] = Field( default_factory=dict, description="A mapping of context keys to values." ) units: Optional[Unit] = Field( diff --git a/tests/test_dkg.py b/tests/test_dkg.py index f7038a075..eaff30852 100644 --- a/tests/test_dkg.py +++ b/tests/test_dkg.py @@ -1,241 +1,241 @@ -"""Tests for the domain knowledge graph app.""" - -import inspect -import os -import unittest -from typing import ClassVar - -import fastapi.params -import pystow -from fastapi.testclient import TestClient -from gilda.grounder import Grounder - -from mira.dkg.api import get_relations -from mira.dkg.client import AskemEntity, Entity, METAREGISTRY_BASE -from mira.dkg.utils import MiraState -from mira.dkg.viz import draw_relations - -MIRA_NEO4J_URL = pystow.get_config("mira", "neo4j_url") or os.getenv("MIRA_NEO4J_URL") - -@unittest.skipIf(not MIRA_NEO4J_URL, reason="Missing neo4j connection configuration") -class TestDKG(unittest.TestCase): - """Test the DKG.""" - - client: ClassVar[TestClient] - - @classmethod - def setUp(cls) -> None: - """Set up the test case.""" - from mira.dkg.wsgi import app - - cls.client = TestClient(app) - cls.client.__enter__() - - @classmethod - def tearDownClass(cls) -> None: - """Clean up the test case.""" - cls.client.__exit__() - - def test_state(self): - """Test the app is filled up with MIRA goodness.""" - self.assertIsInstance(self.client.app.state, MiraState) - self.assertIsInstance(self.client.app.state.grounder, Grounder) - - def test_grounding_get(self): - """Test grounding with a get request.""" - response = self.client.get("/api/ground/vaccine") - self.assertEqual(200, response.status_code, msg=response.content) - self.assertTrue( - any( - r["prefix"] == "vo" and r["identifier"] == "0000001" - for r in response.json()["results"] - ) - ) - - def test_grounding_post(self): - """Test grounding with a post request.""" - response = self.client.post("/api/ground", json={"text": "vaccine"}) - self.assertEqual(200, response.status_code, msg=response.content) - self.assertTrue( - any( - r["prefix"] == "vo" and r["identifier"] == "0000001" - for r in response.json()["results"] - ) - ) - - def test_get_transitive_closure(self): - """Test getting a transitive closure""" - # NOTE: takes ~45 s to run with local Neo4j deployment - response = self.client.get( - "/api/transitive_closure", - params={"relation_types": "subclassof"}, - ) - self.assertEqual( - response.status_code, 200, msg=f"Got status {response.status_code}" - ) - res_json = response.json() - self.assertIsInstance(res_json, list) - self.assertEqual(len(res_json[0]), 2) - self.assertTrue(any(t[0].split(":")[0] == "go" for t in res_json)) - - def test_get_relations(self): - """Test getting relations.""" - spec = inspect.signature(get_relations) - relation_query_default = spec.parameters["relation_query"].default - self.assertIsInstance(relation_query_default, fastapi.params.Body) - for key, data in relation_query_default.examples.items(): - with self.subTest(key=key): - response = self.client.post("/api/relations", json=data["value"]) - self.assertEqual(200, response.status_code, msg=response.content) - - def test_get_relations_graph(self): - """Test getting graph output of relations.""" - spec = inspect.signature(get_relations) - relation_query_default = spec.parameters["relation_query"].default - self.assertIsInstance(relation_query_default, fastapi.params.Body) - for key, data in relation_query_default.examples.items(): - with self.subTest(key=key): - response = self.client.post("/api/relations", json=data["value"]) - is_full = data['value'].get('full', False) - draw_relations(response.json(), f"test_{key}.png", - is_full=is_full) - - def test_search(self): - """Test search functionality.""" - res1 = self.client.get("/api/search", params={ - "q": "infect", "limit": 25, "offset": 0 - }) - self.assertEqual(200, res1.status_code, msg=res1.content) - e1 = [Entity(**e) for e in res1.json()] - - res2 = self.client.get("/api/search", params={ - "q": "infect", "limit": 20, "offset": 5 - }) - self.assertEqual(200, res2.status_code, msg=res2.content) - e2 = [Entity(**e) for e in res2.json()] - self.assertEqual(e1[5:], e2) - - res3 = self.client.get("/api/search", params={ - "q": "count", "limit": 20, "offset": 5, "labels": "unit" - }) - self.assertEqual(200, res3.status_code, msg=res3.content) - e3 = [Entity(**e) for e in res3.json()] - self.assertTrue(all( - "unit" in e.labels - for e in e3 - )) - - res4 = self.client.get("/api/search", params={ - "q": "count", "limit": 20, "offset": 5, "prefixes": "wikidata" - }) - self.assertEqual(200, res3.status_code, msg=res4.content) - e4 = [Entity(**e) for e in res3.json()] - self.assertTrue(all( - "wikidata" == e.prefix - for e in e4 - )) - - # Test not returning restricted prefixes - res5 = self.client.get("/api/search", params={"q": "hasdbxref"}) - self.assertEqual(0, len(res5.json())) - - # Test entries with synonyms but no name, such as fbbt:00000008 - res6 = self.client.get("/api/search", params={"q": "clypeo-labrum"}) - e6 = [Entity(**e) for e in res6.json()] - self.assertTrue(all( - e.id != "fbbt:00000008" - for e in e6 - )) - - def test_entity(self): - """Test getting entities.""" - res = self.client.get("/api/entity/ido:0000463") - e = Entity(**res.json()) - self.assertIsInstance(e, Entity) - self.assertFalse(hasattr(e, "physical_min")) - - res = self.client.get("/api/entity/askemo:0000008") - e = AskemEntity(**res.json()) - self.assertLessEqual(1, len(e.synonyms)) - self.assertTrue( - any( - s.value == "infectivity" and s.type == "oboInOwl:hasExactSynonym" - for s in e.synonyms - ) - ) - self.assertTrue( - any( - xref.id == "ido:0000463" and xref.type == "skos:exactMatch" - for xref in e.xrefs - ) - ) - self.assertEqual("float", e.suggested_data_type) - self.assertEqual("unitless", e.suggested_unit) - - res = self.client.get("/api/entity/askemo:0000010") - self.assertIn("link", res.json()) - e = AskemEntity(**res.json()) - self.assertTrue(hasattr(e, "physical_min")) - self.assertIsInstance(e.physical_min, float) - self.assertEqual(0.0, e.physical_min) - - def test_entities(self): - """Test getting multiple entities.""" - res = self.client.get("/api/entities/ido:0000463,askemo:0000008") - entities = [Entity.from_data(**record) for record in res.json()] - self.assertEqual("ido:0000463", entities[0].id) - self.assertEqual(f"{METAREGISTRY_BASE}/ido:0000463", entities[0].link) - self.assertEqual("askemo:0000008", entities[1].id) - - def test_entity_missing(self): - """Test what happens when an entity is requested that's not in the DKG.""" - # Scenario 1: invalid prefix - res = self.client.get("/api/entity/nope:0000008") - self.assertEqual(404, res.status_code) - - # Scenario 2: invalid identifier - res = self.client.get("/api/entity/askemo:ABCDE") - self.assertEqual(404, res.status_code) - - # Scenario 3: just not in the DKG - res = self.client.get("/api/entity/askemo:1000008") - self.assertEqual(404, res.status_code) - - def test_search_wikidata_fallback(self): - # first, check that without fallback, no results are returned - res = self.client.get("/api/search", params={ - "q": "charles tapley hoyt", "wikidata_fallback": False, - }) - self.assertEqual(200, res.status_code) - entities = [Entity(**e) for e in res.json()] - self.assertEqual([], entities) - - # now, turn on fallback - res = self.client.get("/api/search", params={ - "q": "charles tapley hoyt", "wikidata_fallback": True, - }) - self.assertEqual(200, res.status_code) - entities = [Entity(**e) for e in res.json()] - self.assertTrue(any( - e.id == "wikidata:Q47475003" - for e in entities - )) - self.assertEqual([], entities) - - def test_parent_query(self): - """Test parent query.""" - res = self.client.post( - "/api/common_parent", - json={"curie1": "ido:0000566", - "curie2": "ido:0000567"} - ) - self.assertEqual(200, res.status_code) - - # Try to parse the json to an Entity object - entities = [Entity(**r) for r in res.json()] - - # Check that the parent is correct - assert len(entities) == 1 - entity = entities[0] - assert entity.id == "ido:0000504" - assert not entity.obsolete +# """Tests for the domain knowledge graph app.""" +# +# import inspect +# import os +# import unittest +# from typing import ClassVar +# +# import fastapi.params +# import pystow +# from fastapi.testclient import TestClient +# from gilda.grounder import Grounder +# +# from mira.dkg.api import get_relations +# from mira.dkg.client import AskemEntity, Entity, METAREGISTRY_BASE +# from mira.dkg.utils import MiraState +# from mira.dkg.viz import draw_relations +# +# MIRA_NEO4J_URL = pystow.get_config("mira", "neo4j_url") or os.getenv("MIRA_NEO4J_URL") +# +# @unittest.skipIf(not MIRA_NEO4J_URL, reason="Missing neo4j connection configuration") +# class TestDKG(unittest.TestCase): +# """Test the DKG.""" +# +# client: ClassVar[TestClient] +# +# @classmethod +# def setUp(cls) -> None: +# """Set up the test case.""" +# from mira.dkg.wsgi import app +# +# cls.client = TestClient(app) +# cls.client.__enter__() +# +# @classmethod +# def tearDownClass(cls) -> None: +# """Clean up the test case.""" +# cls.client.__exit__() +# +# def test_state(self): +# """Test the app is filled up with MIRA goodness.""" +# self.assertIsInstance(self.client.app.state, MiraState) +# self.assertIsInstance(self.client.app.state.grounder, Grounder) +# +# def test_grounding_get(self): +# """Test grounding with a get request.""" +# response = self.client.get("/api/ground/vaccine") +# self.assertEqual(200, response.status_code, msg=response.content) +# self.assertTrue( +# any( +# r["prefix"] == "vo" and r["identifier"] == "0000001" +# for r in response.json()["results"] +# ) +# ) +# +# def test_grounding_post(self): +# """Test grounding with a post request.""" +# response = self.client.post("/api/ground", json={"text": "vaccine"}) +# self.assertEqual(200, response.status_code, msg=response.content) +# self.assertTrue( +# any( +# r["prefix"] == "vo" and r["identifier"] == "0000001" +# for r in response.json()["results"] +# ) +# ) +# +# def test_get_transitive_closure(self): +# """Test getting a transitive closure""" +# # NOTE: takes ~45 s to run with local Neo4j deployment +# response = self.client.get( +# "/api/transitive_closure", +# params={"relation_types": "subclassof"}, +# ) +# self.assertEqual( +# response.status_code, 200, msg=f"Got status {response.status_code}" +# ) +# res_json = response.json() +# self.assertIsInstance(res_json, list) +# self.assertEqual(len(res_json[0]), 2) +# self.assertTrue(any(t[0].split(":")[0] == "go" for t in res_json)) +# +# def test_get_relations(self): +# """Test getting relations.""" +# spec = inspect.signature(get_relations) +# relation_query_default = spec.parameters["relation_query"].default +# self.assertIsInstance(relation_query_default, fastapi.params.Body) +# for key, data in relation_query_default.examples.items(): +# with self.subTest(key=key): +# response = self.client.post("/api/relations", json=data["value"]) +# self.assertEqual(200, response.status_code, msg=response.content) +# +# def test_get_relations_graph(self): +# """Test getting graph output of relations.""" +# spec = inspect.signature(get_relations) +# relation_query_default = spec.parameters["relation_query"].default +# self.assertIsInstance(relation_query_default, fastapi.params.Body) +# for key, data in relation_query_default.examples.items(): +# with self.subTest(key=key): +# response = self.client.post("/api/relations", json=data["value"]) +# is_full = data['value'].get('full', False) +# draw_relations(response.json(), f"test_{key}.png", +# is_full=is_full) +# +# def test_search(self): +# """Test search functionality.""" +# res1 = self.client.get("/api/search", params={ +# "q": "infect", "limit": 25, "offset": 0 +# }) +# self.assertEqual(200, res1.status_code, msg=res1.content) +# e1 = [Entity(**e) for e in res1.json()] +# +# res2 = self.client.get("/api/search", params={ +# "q": "infect", "limit": 20, "offset": 5 +# }) +# self.assertEqual(200, res2.status_code, msg=res2.content) +# e2 = [Entity(**e) for e in res2.json()] +# self.assertEqual(e1[5:], e2) +# +# res3 = self.client.get("/api/search", params={ +# "q": "count", "limit": 20, "offset": 5, "labels": "unit" +# }) +# self.assertEqual(200, res3.status_code, msg=res3.content) +# e3 = [Entity(**e) for e in res3.json()] +# self.assertTrue(all( +# "unit" in e.labels +# for e in e3 +# )) +# +# res4 = self.client.get("/api/search", params={ +# "q": "count", "limit": 20, "offset": 5, "prefixes": "wikidata" +# }) +# self.assertEqual(200, res3.status_code, msg=res4.content) +# e4 = [Entity(**e) for e in res3.json()] +# self.assertTrue(all( +# "wikidata" == e.prefix +# for e in e4 +# )) +# +# # Test not returning restricted prefixes +# res5 = self.client.get("/api/search", params={"q": "hasdbxref"}) +# self.assertEqual(0, len(res5.json())) +# +# # Test entries with synonyms but no name, such as fbbt:00000008 +# res6 = self.client.get("/api/search", params={"q": "clypeo-labrum"}) +# e6 = [Entity(**e) for e in res6.json()] +# self.assertTrue(all( +# e.id != "fbbt:00000008" +# for e in e6 +# )) +# +# def test_entity(self): +# """Test getting entities.""" +# res = self.client.get("/api/entity/ido:0000463") +# e = Entity(**res.json()) +# self.assertIsInstance(e, Entity) +# self.assertFalse(hasattr(e, "physical_min")) +# +# res = self.client.get("/api/entity/askemo:0000008") +# e = AskemEntity(**res.json()) +# self.assertLessEqual(1, len(e.synonyms)) +# self.assertTrue( +# any( +# s.value == "infectivity" and s.type == "oboInOwl:hasExactSynonym" +# for s in e.synonyms +# ) +# ) +# self.assertTrue( +# any( +# xref.id == "ido:0000463" and xref.type == "skos:exactMatch" +# for xref in e.xrefs +# ) +# ) +# self.assertEqual("float", e.suggested_data_type) +# self.assertEqual("unitless", e.suggested_unit) +# +# res = self.client.get("/api/entity/askemo:0000010") +# self.assertIn("link", res.json()) +# e = AskemEntity(**res.json()) +# self.assertTrue(hasattr(e, "physical_min")) +# self.assertIsInstance(e.physical_min, float) +# self.assertEqual(0.0, e.physical_min) +# +# def test_entities(self): +# """Test getting multiple entities.""" +# res = self.client.get("/api/entities/ido:0000463,askemo:0000008") +# entities = [Entity.from_data(**record) for record in res.json()] +# self.assertEqual("ido:0000463", entities[0].id) +# self.assertEqual(f"{METAREGISTRY_BASE}/ido:0000463", entities[0].link) +# self.assertEqual("askemo:0000008", entities[1].id) +# +# def test_entity_missing(self): +# """Test what happens when an entity is requested that's not in the DKG.""" +# # Scenario 1: invalid prefix +# res = self.client.get("/api/entity/nope:0000008") +# self.assertEqual(404, res.status_code) +# +# # Scenario 2: invalid identifier +# res = self.client.get("/api/entity/askemo:ABCDE") +# self.assertEqual(404, res.status_code) +# +# # Scenario 3: just not in the DKG +# res = self.client.get("/api/entity/askemo:1000008") +# self.assertEqual(404, res.status_code) +# +# def test_search_wikidata_fallback(self): +# # first, check that without fallback, no results are returned +# res = self.client.get("/api/search", params={ +# "q": "charles tapley hoyt", "wikidata_fallback": False, +# }) +# self.assertEqual(200, res.status_code) +# entities = [Entity(**e) for e in res.json()] +# self.assertEqual([], entities) +# +# # now, turn on fallback +# res = self.client.get("/api/search", params={ +# "q": "charles tapley hoyt", "wikidata_fallback": True, +# }) +# self.assertEqual(200, res.status_code) +# entities = [Entity(**e) for e in res.json()] +# self.assertTrue(any( +# e.id == "wikidata:Q47475003" +# for e in entities +# )) +# self.assertEqual([], entities) +# +# def test_parent_query(self): +# """Test parent query.""" +# res = self.client.post( +# "/api/common_parent", +# json={"curie1": "ido:0000566", +# "curie2": "ido:0000567"} +# ) +# self.assertEqual(200, res.status_code) +# +# # Try to parse the json to an Entity object +# entities = [Entity(**r) for r in res.json()] +# +# # Check that the parent is correct +# assert len(entities) == 1 +# entity = entities[0] +# assert entity.id == "ido:0000504" +# assert not entity.obsolete diff --git a/tests/test_graph_export.py b/tests/test_graph_export.py index 6626046bc..ddb4e1004 100644 --- a/tests/test_graph_export.py +++ b/tests/test_graph_export.py @@ -1,129 +1,129 @@ -from itertools import product, chain - -from mira.examples.sir import sir -from mira.metamodel import TemplateModel, Concept -from mira.metamodel.comparison import TemplateModelComparison -from mira.dkg.web_client import is_ontological_child_web - - -def test_template_model_comp_graph_export(): - # Check counts - # Check identifications - # check expected edges - - # This will create a copy SIR model with context -> should have a - # refinement edge between each corresponding pair of nodes in the graph - sir_w_context = TemplateModel( - templates=[ - t.with_context(location="geonames:4930956") for t in sir.templates - ], - parameters=sir.parameters, - initials=sir.initials, - ) - tmc = TemplateModelComparison( - template_models=[sir, sir_w_context], - refinement_func=is_ontological_child_web, - ) - - # Check that the graph export is correct - graph_data = tmc.model_comparison - - # Check that model ids are integers and non-negative - assert len(graph_data.concept_nodes.values()) > 0 - # {model_id: {node_id: Concept|Template}} - assert isinstance(list(graph_data.template_nodes.values())[0], dict) - assert isinstance(list(list(graph_data.concept_nodes.values())[0].values())[0], - Concept) - assert list(list(graph_data.concept_nodes.values())[0].keys())[0] >= 0 - assert all(isinstance(k, int) for k in graph_data.template_models.keys()) - assert all(k >= 0 for k in graph_data.template_models.keys()) - model_id_refs = {k for k in graph_data.template_models.keys()} - - model_id_refs_nodes = {model_id for model_id in graph_data.concept_nodes.keys()} - assert model_id_refs == model_id_refs_nodes - - # One node per template per TemplateModel + one node per concept per - # template per TemplateModel - template_node_count = len(sir.templates) + len(sir_w_context.templates) - assert template_node_count == 4 - - concept_keys = set() - for t in chain(sir.templates, sir_w_context.templates): - for c in t.get_concepts(): - concept_keys.add(c.get_key()) - - concept_node_count = len(concept_keys) - assert concept_node_count == 6 - assert 10 == template_node_count + concept_node_count - - # Check that all models are represented in the node lookup - assert len(graph_data.template_nodes) == len(graph_data.template_models) - - # Check that the total count of nodes is as expected - total_count = 0 - for nodes in graph_data.template_nodes.values(): - total_count += len(nodes) - assert total_count == template_node_count - - total_count = 0 - for nodes in graph_data.concept_nodes.values(): - total_count += len(nodes) - assert total_count == concept_node_count - - # One intra edge per concept per template per TemplateModel - assert len(graph_data.intra_model_edges) == concept_node_count + template_node_count - - # (One inter edge per refinement + one inter edge per equality) per TemplateModel - concept_equal_edges = 0 - template_equal_edges = 0 - concept_refinement_edges = 0 - template_refinement_edges = 0 - seen_concept_pairs = set() - for t1, t2 in product(sir.templates, sir_w_context.templates): - if t1.is_equal_to(t2, with_context=True): - template_equal_edges += 1 - if t1.refinement_of( - t2, refinement_func=is_ontological_child_web, with_context=True - ): - template_refinement_edges += 1 - if t2.refinement_of( - t1, refinement_func=is_ontological_child_web, with_context=True - ): - template_refinement_edges += 1 - for c1, c2 in product(t1.get_concepts(), t2.get_concepts()): - if (c1.get_key(), c2.get_key()) in seen_concept_pairs: - continue - if c1.is_equal_to(c2, with_context=True): - concept_equal_edges += 1 - if c1.refinement_of( - c2, refinement_func=is_ontological_child_web, with_context=True - ): - concept_refinement_edges += 1 - if c2.refinement_of( - c1, refinement_func=is_ontological_child_web, with_context=True - ): - concept_refinement_edges += 1 - seen_concept_pairs.add((c1.get_key(), c2.get_key())) - - # Check that the counts are as expected - assert template_equal_edges == 0 - assert template_refinement_edges == 2 - assert concept_equal_edges == 0 - assert concept_refinement_edges == 3 - - # check that the total number of inter edges is as expected and - # corresponds to what's in the graph data - inter_edge_count = ( - template_equal_edges - + template_refinement_edges - + concept_refinement_edges - + concept_equal_edges - ) - assert inter_edge_count == 5 - assert len(graph_data.inter_model_edges) == inter_edge_count - - # Score should be 0.5*number of refinement edges + 1*number of equal edges - sim_score = graph_data.get_similarity_score(0, 1) - assert sim_score == (0.5 * concept_refinement_edges + - concept_equal_edges) / 3 - assert sim_score == 1.5 / 3 +# from itertools import product, chain +# +# from mira.examples.sir import sir +# from mira.metamodel import TemplateModel, Concept +# from mira.metamodel.comparison import TemplateModelComparison +# from mira.dkg.web_client import is_ontological_child_web +# +# +# def test_template_model_comp_graph_export(): +# # Check counts +# # Check identifications +# # check expected edges +# +# # This will create a copy SIR model with context -> should have a +# # refinement edge between each corresponding pair of nodes in the graph +# sir_w_context = TemplateModel( +# templates=[ +# t.with_context(location="geonames:4930956") for t in sir.templates +# ], +# parameters=sir.parameters, +# initials=sir.initials, +# ) +# tmc = TemplateModelComparison( +# template_models=[sir, sir_w_context], +# refinement_func=is_ontological_child_web, +# ) +# +# # Check that the graph export is correct +# graph_data = tmc.model_comparison +# +# # Check that model ids are integers and non-negative +# assert len(graph_data.concept_nodes.values()) > 0 +# # {model_id: {node_id: Concept|Template}} +# assert isinstance(list(graph_data.template_nodes.values())[0], dict) +# assert isinstance(list(list(graph_data.concept_nodes.values())[0].values())[0], +# Concept) +# assert list(list(graph_data.concept_nodes.values())[0].keys())[0] >= 0 +# assert all(isinstance(k, int) for k in graph_data.template_models.keys()) +# assert all(k >= 0 for k in graph_data.template_models.keys()) +# model_id_refs = {k for k in graph_data.template_models.keys()} +# +# model_id_refs_nodes = {model_id for model_id in graph_data.concept_nodes.keys()} +# assert model_id_refs == model_id_refs_nodes +# +# # One node per template per TemplateModel + one node per concept per +# # template per TemplateModel +# template_node_count = len(sir.templates) + len(sir_w_context.templates) +# assert template_node_count == 4 +# +# concept_keys = set() +# for t in chain(sir.templates, sir_w_context.templates): +# for c in t.get_concepts(): +# concept_keys.add(c.get_key()) +# +# concept_node_count = len(concept_keys) +# assert concept_node_count == 6 +# assert 10 == template_node_count + concept_node_count +# +# # Check that all models are represented in the node lookup +# assert len(graph_data.template_nodes) == len(graph_data.template_models) +# +# # Check that the total count of nodes is as expected +# total_count = 0 +# for nodes in graph_data.template_nodes.values(): +# total_count += len(nodes) +# assert total_count == template_node_count +# +# total_count = 0 +# for nodes in graph_data.concept_nodes.values(): +# total_count += len(nodes) +# assert total_count == concept_node_count +# +# # One intra edge per concept per template per TemplateModel +# assert len(graph_data.intra_model_edges) == concept_node_count + template_node_count +# +# # (One inter edge per refinement + one inter edge per equality) per TemplateModel +# concept_equal_edges = 0 +# template_equal_edges = 0 +# concept_refinement_edges = 0 +# template_refinement_edges = 0 +# seen_concept_pairs = set() +# for t1, t2 in product(sir.templates, sir_w_context.templates): +# if t1.is_equal_to(t2, with_context=True): +# template_equal_edges += 1 +# if t1.refinement_of( +# t2, refinement_func=is_ontological_child_web, with_context=True +# ): +# template_refinement_edges += 1 +# if t2.refinement_of( +# t1, refinement_func=is_ontological_child_web, with_context=True +# ): +# template_refinement_edges += 1 +# for c1, c2 in product(t1.get_concepts(), t2.get_concepts()): +# if (c1.get_key(), c2.get_key()) in seen_concept_pairs: +# continue +# if c1.is_equal_to(c2, with_context=True): +# concept_equal_edges += 1 +# if c1.refinement_of( +# c2, refinement_func=is_ontological_child_web, with_context=True +# ): +# concept_refinement_edges += 1 +# if c2.refinement_of( +# c1, refinement_func=is_ontological_child_web, with_context=True +# ): +# concept_refinement_edges += 1 +# seen_concept_pairs.add((c1.get_key(), c2.get_key())) +# +# # Check that the counts are as expected +# assert template_equal_edges == 0 +# assert template_refinement_edges == 2 +# assert concept_equal_edges == 0 +# assert concept_refinement_edges == 3 +# +# # check that the total number of inter edges is as expected and +# # corresponds to what's in the graph data +# inter_edge_count = ( +# template_equal_edges +# + template_refinement_edges +# + concept_refinement_edges +# + concept_equal_edges +# ) +# assert inter_edge_count == 5 +# assert len(graph_data.inter_model_edges) == inter_edge_count +# +# # Score should be 0.5*number of refinement edges + 1*number of equal edges +# sim_score = graph_data.get_similarity_score(0, 1) +# assert sim_score == (0.5 * concept_refinement_edges + +# concept_equal_edges) / 3 +# assert sim_score == 1.5 / 3 From 713208be1262c337235c75f899868b4c7b6ce09e Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 9 Sep 2024 15:21:53 -0400 Subject: [PATCH 13/32] Install httpx under test block --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 1f05bf218..15ef4f16f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ tests = pandas matplotlib matplotlib_venn + httpx dkg-client = neo4j networkx From 77ceb206684fe48136424e06bb0d5639661ee7ec Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 9 Sep 2024 15:31:43 -0400 Subject: [PATCH 14/32] Uncomment test code and remove httpx from dependencies --- setup.cfg | 1 - tests/test_dkg.py | 482 ++++++++++++++++++------------------- tests/test_graph_export.py | 258 ++++++++++---------- 3 files changed, 370 insertions(+), 371 deletions(-) diff --git a/setup.cfg b/setup.cfg index 15ef4f16f..1f05bf218 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,7 +47,6 @@ tests = pandas matplotlib matplotlib_venn - httpx dkg-client = neo4j networkx diff --git a/tests/test_dkg.py b/tests/test_dkg.py index eaff30852..f7038a075 100644 --- a/tests/test_dkg.py +++ b/tests/test_dkg.py @@ -1,241 +1,241 @@ -# """Tests for the domain knowledge graph app.""" -# -# import inspect -# import os -# import unittest -# from typing import ClassVar -# -# import fastapi.params -# import pystow -# from fastapi.testclient import TestClient -# from gilda.grounder import Grounder -# -# from mira.dkg.api import get_relations -# from mira.dkg.client import AskemEntity, Entity, METAREGISTRY_BASE -# from mira.dkg.utils import MiraState -# from mira.dkg.viz import draw_relations -# -# MIRA_NEO4J_URL = pystow.get_config("mira", "neo4j_url") or os.getenv("MIRA_NEO4J_URL") -# -# @unittest.skipIf(not MIRA_NEO4J_URL, reason="Missing neo4j connection configuration") -# class TestDKG(unittest.TestCase): -# """Test the DKG.""" -# -# client: ClassVar[TestClient] -# -# @classmethod -# def setUp(cls) -> None: -# """Set up the test case.""" -# from mira.dkg.wsgi import app -# -# cls.client = TestClient(app) -# cls.client.__enter__() -# -# @classmethod -# def tearDownClass(cls) -> None: -# """Clean up the test case.""" -# cls.client.__exit__() -# -# def test_state(self): -# """Test the app is filled up with MIRA goodness.""" -# self.assertIsInstance(self.client.app.state, MiraState) -# self.assertIsInstance(self.client.app.state.grounder, Grounder) -# -# def test_grounding_get(self): -# """Test grounding with a get request.""" -# response = self.client.get("/api/ground/vaccine") -# self.assertEqual(200, response.status_code, msg=response.content) -# self.assertTrue( -# any( -# r["prefix"] == "vo" and r["identifier"] == "0000001" -# for r in response.json()["results"] -# ) -# ) -# -# def test_grounding_post(self): -# """Test grounding with a post request.""" -# response = self.client.post("/api/ground", json={"text": "vaccine"}) -# self.assertEqual(200, response.status_code, msg=response.content) -# self.assertTrue( -# any( -# r["prefix"] == "vo" and r["identifier"] == "0000001" -# for r in response.json()["results"] -# ) -# ) -# -# def test_get_transitive_closure(self): -# """Test getting a transitive closure""" -# # NOTE: takes ~45 s to run with local Neo4j deployment -# response = self.client.get( -# "/api/transitive_closure", -# params={"relation_types": "subclassof"}, -# ) -# self.assertEqual( -# response.status_code, 200, msg=f"Got status {response.status_code}" -# ) -# res_json = response.json() -# self.assertIsInstance(res_json, list) -# self.assertEqual(len(res_json[0]), 2) -# self.assertTrue(any(t[0].split(":")[0] == "go" for t in res_json)) -# -# def test_get_relations(self): -# """Test getting relations.""" -# spec = inspect.signature(get_relations) -# relation_query_default = spec.parameters["relation_query"].default -# self.assertIsInstance(relation_query_default, fastapi.params.Body) -# for key, data in relation_query_default.examples.items(): -# with self.subTest(key=key): -# response = self.client.post("/api/relations", json=data["value"]) -# self.assertEqual(200, response.status_code, msg=response.content) -# -# def test_get_relations_graph(self): -# """Test getting graph output of relations.""" -# spec = inspect.signature(get_relations) -# relation_query_default = spec.parameters["relation_query"].default -# self.assertIsInstance(relation_query_default, fastapi.params.Body) -# for key, data in relation_query_default.examples.items(): -# with self.subTest(key=key): -# response = self.client.post("/api/relations", json=data["value"]) -# is_full = data['value'].get('full', False) -# draw_relations(response.json(), f"test_{key}.png", -# is_full=is_full) -# -# def test_search(self): -# """Test search functionality.""" -# res1 = self.client.get("/api/search", params={ -# "q": "infect", "limit": 25, "offset": 0 -# }) -# self.assertEqual(200, res1.status_code, msg=res1.content) -# e1 = [Entity(**e) for e in res1.json()] -# -# res2 = self.client.get("/api/search", params={ -# "q": "infect", "limit": 20, "offset": 5 -# }) -# self.assertEqual(200, res2.status_code, msg=res2.content) -# e2 = [Entity(**e) for e in res2.json()] -# self.assertEqual(e1[5:], e2) -# -# res3 = self.client.get("/api/search", params={ -# "q": "count", "limit": 20, "offset": 5, "labels": "unit" -# }) -# self.assertEqual(200, res3.status_code, msg=res3.content) -# e3 = [Entity(**e) for e in res3.json()] -# self.assertTrue(all( -# "unit" in e.labels -# for e in e3 -# )) -# -# res4 = self.client.get("/api/search", params={ -# "q": "count", "limit": 20, "offset": 5, "prefixes": "wikidata" -# }) -# self.assertEqual(200, res3.status_code, msg=res4.content) -# e4 = [Entity(**e) for e in res3.json()] -# self.assertTrue(all( -# "wikidata" == e.prefix -# for e in e4 -# )) -# -# # Test not returning restricted prefixes -# res5 = self.client.get("/api/search", params={"q": "hasdbxref"}) -# self.assertEqual(0, len(res5.json())) -# -# # Test entries with synonyms but no name, such as fbbt:00000008 -# res6 = self.client.get("/api/search", params={"q": "clypeo-labrum"}) -# e6 = [Entity(**e) for e in res6.json()] -# self.assertTrue(all( -# e.id != "fbbt:00000008" -# for e in e6 -# )) -# -# def test_entity(self): -# """Test getting entities.""" -# res = self.client.get("/api/entity/ido:0000463") -# e = Entity(**res.json()) -# self.assertIsInstance(e, Entity) -# self.assertFalse(hasattr(e, "physical_min")) -# -# res = self.client.get("/api/entity/askemo:0000008") -# e = AskemEntity(**res.json()) -# self.assertLessEqual(1, len(e.synonyms)) -# self.assertTrue( -# any( -# s.value == "infectivity" and s.type == "oboInOwl:hasExactSynonym" -# for s in e.synonyms -# ) -# ) -# self.assertTrue( -# any( -# xref.id == "ido:0000463" and xref.type == "skos:exactMatch" -# for xref in e.xrefs -# ) -# ) -# self.assertEqual("float", e.suggested_data_type) -# self.assertEqual("unitless", e.suggested_unit) -# -# res = self.client.get("/api/entity/askemo:0000010") -# self.assertIn("link", res.json()) -# e = AskemEntity(**res.json()) -# self.assertTrue(hasattr(e, "physical_min")) -# self.assertIsInstance(e.physical_min, float) -# self.assertEqual(0.0, e.physical_min) -# -# def test_entities(self): -# """Test getting multiple entities.""" -# res = self.client.get("/api/entities/ido:0000463,askemo:0000008") -# entities = [Entity.from_data(**record) for record in res.json()] -# self.assertEqual("ido:0000463", entities[0].id) -# self.assertEqual(f"{METAREGISTRY_BASE}/ido:0000463", entities[0].link) -# self.assertEqual("askemo:0000008", entities[1].id) -# -# def test_entity_missing(self): -# """Test what happens when an entity is requested that's not in the DKG.""" -# # Scenario 1: invalid prefix -# res = self.client.get("/api/entity/nope:0000008") -# self.assertEqual(404, res.status_code) -# -# # Scenario 2: invalid identifier -# res = self.client.get("/api/entity/askemo:ABCDE") -# self.assertEqual(404, res.status_code) -# -# # Scenario 3: just not in the DKG -# res = self.client.get("/api/entity/askemo:1000008") -# self.assertEqual(404, res.status_code) -# -# def test_search_wikidata_fallback(self): -# # first, check that without fallback, no results are returned -# res = self.client.get("/api/search", params={ -# "q": "charles tapley hoyt", "wikidata_fallback": False, -# }) -# self.assertEqual(200, res.status_code) -# entities = [Entity(**e) for e in res.json()] -# self.assertEqual([], entities) -# -# # now, turn on fallback -# res = self.client.get("/api/search", params={ -# "q": "charles tapley hoyt", "wikidata_fallback": True, -# }) -# self.assertEqual(200, res.status_code) -# entities = [Entity(**e) for e in res.json()] -# self.assertTrue(any( -# e.id == "wikidata:Q47475003" -# for e in entities -# )) -# self.assertEqual([], entities) -# -# def test_parent_query(self): -# """Test parent query.""" -# res = self.client.post( -# "/api/common_parent", -# json={"curie1": "ido:0000566", -# "curie2": "ido:0000567"} -# ) -# self.assertEqual(200, res.status_code) -# -# # Try to parse the json to an Entity object -# entities = [Entity(**r) for r in res.json()] -# -# # Check that the parent is correct -# assert len(entities) == 1 -# entity = entities[0] -# assert entity.id == "ido:0000504" -# assert not entity.obsolete +"""Tests for the domain knowledge graph app.""" + +import inspect +import os +import unittest +from typing import ClassVar + +import fastapi.params +import pystow +from fastapi.testclient import TestClient +from gilda.grounder import Grounder + +from mira.dkg.api import get_relations +from mira.dkg.client import AskemEntity, Entity, METAREGISTRY_BASE +from mira.dkg.utils import MiraState +from mira.dkg.viz import draw_relations + +MIRA_NEO4J_URL = pystow.get_config("mira", "neo4j_url") or os.getenv("MIRA_NEO4J_URL") + +@unittest.skipIf(not MIRA_NEO4J_URL, reason="Missing neo4j connection configuration") +class TestDKG(unittest.TestCase): + """Test the DKG.""" + + client: ClassVar[TestClient] + + @classmethod + def setUp(cls) -> None: + """Set up the test case.""" + from mira.dkg.wsgi import app + + cls.client = TestClient(app) + cls.client.__enter__() + + @classmethod + def tearDownClass(cls) -> None: + """Clean up the test case.""" + cls.client.__exit__() + + def test_state(self): + """Test the app is filled up with MIRA goodness.""" + self.assertIsInstance(self.client.app.state, MiraState) + self.assertIsInstance(self.client.app.state.grounder, Grounder) + + def test_grounding_get(self): + """Test grounding with a get request.""" + response = self.client.get("/api/ground/vaccine") + self.assertEqual(200, response.status_code, msg=response.content) + self.assertTrue( + any( + r["prefix"] == "vo" and r["identifier"] == "0000001" + for r in response.json()["results"] + ) + ) + + def test_grounding_post(self): + """Test grounding with a post request.""" + response = self.client.post("/api/ground", json={"text": "vaccine"}) + self.assertEqual(200, response.status_code, msg=response.content) + self.assertTrue( + any( + r["prefix"] == "vo" and r["identifier"] == "0000001" + for r in response.json()["results"] + ) + ) + + def test_get_transitive_closure(self): + """Test getting a transitive closure""" + # NOTE: takes ~45 s to run with local Neo4j deployment + response = self.client.get( + "/api/transitive_closure", + params={"relation_types": "subclassof"}, + ) + self.assertEqual( + response.status_code, 200, msg=f"Got status {response.status_code}" + ) + res_json = response.json() + self.assertIsInstance(res_json, list) + self.assertEqual(len(res_json[0]), 2) + self.assertTrue(any(t[0].split(":")[0] == "go" for t in res_json)) + + def test_get_relations(self): + """Test getting relations.""" + spec = inspect.signature(get_relations) + relation_query_default = spec.parameters["relation_query"].default + self.assertIsInstance(relation_query_default, fastapi.params.Body) + for key, data in relation_query_default.examples.items(): + with self.subTest(key=key): + response = self.client.post("/api/relations", json=data["value"]) + self.assertEqual(200, response.status_code, msg=response.content) + + def test_get_relations_graph(self): + """Test getting graph output of relations.""" + spec = inspect.signature(get_relations) + relation_query_default = spec.parameters["relation_query"].default + self.assertIsInstance(relation_query_default, fastapi.params.Body) + for key, data in relation_query_default.examples.items(): + with self.subTest(key=key): + response = self.client.post("/api/relations", json=data["value"]) + is_full = data['value'].get('full', False) + draw_relations(response.json(), f"test_{key}.png", + is_full=is_full) + + def test_search(self): + """Test search functionality.""" + res1 = self.client.get("/api/search", params={ + "q": "infect", "limit": 25, "offset": 0 + }) + self.assertEqual(200, res1.status_code, msg=res1.content) + e1 = [Entity(**e) for e in res1.json()] + + res2 = self.client.get("/api/search", params={ + "q": "infect", "limit": 20, "offset": 5 + }) + self.assertEqual(200, res2.status_code, msg=res2.content) + e2 = [Entity(**e) for e in res2.json()] + self.assertEqual(e1[5:], e2) + + res3 = self.client.get("/api/search", params={ + "q": "count", "limit": 20, "offset": 5, "labels": "unit" + }) + self.assertEqual(200, res3.status_code, msg=res3.content) + e3 = [Entity(**e) for e in res3.json()] + self.assertTrue(all( + "unit" in e.labels + for e in e3 + )) + + res4 = self.client.get("/api/search", params={ + "q": "count", "limit": 20, "offset": 5, "prefixes": "wikidata" + }) + self.assertEqual(200, res3.status_code, msg=res4.content) + e4 = [Entity(**e) for e in res3.json()] + self.assertTrue(all( + "wikidata" == e.prefix + for e in e4 + )) + + # Test not returning restricted prefixes + res5 = self.client.get("/api/search", params={"q": "hasdbxref"}) + self.assertEqual(0, len(res5.json())) + + # Test entries with synonyms but no name, such as fbbt:00000008 + res6 = self.client.get("/api/search", params={"q": "clypeo-labrum"}) + e6 = [Entity(**e) for e in res6.json()] + self.assertTrue(all( + e.id != "fbbt:00000008" + for e in e6 + )) + + def test_entity(self): + """Test getting entities.""" + res = self.client.get("/api/entity/ido:0000463") + e = Entity(**res.json()) + self.assertIsInstance(e, Entity) + self.assertFalse(hasattr(e, "physical_min")) + + res = self.client.get("/api/entity/askemo:0000008") + e = AskemEntity(**res.json()) + self.assertLessEqual(1, len(e.synonyms)) + self.assertTrue( + any( + s.value == "infectivity" and s.type == "oboInOwl:hasExactSynonym" + for s in e.synonyms + ) + ) + self.assertTrue( + any( + xref.id == "ido:0000463" and xref.type == "skos:exactMatch" + for xref in e.xrefs + ) + ) + self.assertEqual("float", e.suggested_data_type) + self.assertEqual("unitless", e.suggested_unit) + + res = self.client.get("/api/entity/askemo:0000010") + self.assertIn("link", res.json()) + e = AskemEntity(**res.json()) + self.assertTrue(hasattr(e, "physical_min")) + self.assertIsInstance(e.physical_min, float) + self.assertEqual(0.0, e.physical_min) + + def test_entities(self): + """Test getting multiple entities.""" + res = self.client.get("/api/entities/ido:0000463,askemo:0000008") + entities = [Entity.from_data(**record) for record in res.json()] + self.assertEqual("ido:0000463", entities[0].id) + self.assertEqual(f"{METAREGISTRY_BASE}/ido:0000463", entities[0].link) + self.assertEqual("askemo:0000008", entities[1].id) + + def test_entity_missing(self): + """Test what happens when an entity is requested that's not in the DKG.""" + # Scenario 1: invalid prefix + res = self.client.get("/api/entity/nope:0000008") + self.assertEqual(404, res.status_code) + + # Scenario 2: invalid identifier + res = self.client.get("/api/entity/askemo:ABCDE") + self.assertEqual(404, res.status_code) + + # Scenario 3: just not in the DKG + res = self.client.get("/api/entity/askemo:1000008") + self.assertEqual(404, res.status_code) + + def test_search_wikidata_fallback(self): + # first, check that without fallback, no results are returned + res = self.client.get("/api/search", params={ + "q": "charles tapley hoyt", "wikidata_fallback": False, + }) + self.assertEqual(200, res.status_code) + entities = [Entity(**e) for e in res.json()] + self.assertEqual([], entities) + + # now, turn on fallback + res = self.client.get("/api/search", params={ + "q": "charles tapley hoyt", "wikidata_fallback": True, + }) + self.assertEqual(200, res.status_code) + entities = [Entity(**e) for e in res.json()] + self.assertTrue(any( + e.id == "wikidata:Q47475003" + for e in entities + )) + self.assertEqual([], entities) + + def test_parent_query(self): + """Test parent query.""" + res = self.client.post( + "/api/common_parent", + json={"curie1": "ido:0000566", + "curie2": "ido:0000567"} + ) + self.assertEqual(200, res.status_code) + + # Try to parse the json to an Entity object + entities = [Entity(**r) for r in res.json()] + + # Check that the parent is correct + assert len(entities) == 1 + entity = entities[0] + assert entity.id == "ido:0000504" + assert not entity.obsolete diff --git a/tests/test_graph_export.py b/tests/test_graph_export.py index ddb4e1004..6626046bc 100644 --- a/tests/test_graph_export.py +++ b/tests/test_graph_export.py @@ -1,129 +1,129 @@ -# from itertools import product, chain -# -# from mira.examples.sir import sir -# from mira.metamodel import TemplateModel, Concept -# from mira.metamodel.comparison import TemplateModelComparison -# from mira.dkg.web_client import is_ontological_child_web -# -# -# def test_template_model_comp_graph_export(): -# # Check counts -# # Check identifications -# # check expected edges -# -# # This will create a copy SIR model with context -> should have a -# # refinement edge between each corresponding pair of nodes in the graph -# sir_w_context = TemplateModel( -# templates=[ -# t.with_context(location="geonames:4930956") for t in sir.templates -# ], -# parameters=sir.parameters, -# initials=sir.initials, -# ) -# tmc = TemplateModelComparison( -# template_models=[sir, sir_w_context], -# refinement_func=is_ontological_child_web, -# ) -# -# # Check that the graph export is correct -# graph_data = tmc.model_comparison -# -# # Check that model ids are integers and non-negative -# assert len(graph_data.concept_nodes.values()) > 0 -# # {model_id: {node_id: Concept|Template}} -# assert isinstance(list(graph_data.template_nodes.values())[0], dict) -# assert isinstance(list(list(graph_data.concept_nodes.values())[0].values())[0], -# Concept) -# assert list(list(graph_data.concept_nodes.values())[0].keys())[0] >= 0 -# assert all(isinstance(k, int) for k in graph_data.template_models.keys()) -# assert all(k >= 0 for k in graph_data.template_models.keys()) -# model_id_refs = {k for k in graph_data.template_models.keys()} -# -# model_id_refs_nodes = {model_id for model_id in graph_data.concept_nodes.keys()} -# assert model_id_refs == model_id_refs_nodes -# -# # One node per template per TemplateModel + one node per concept per -# # template per TemplateModel -# template_node_count = len(sir.templates) + len(sir_w_context.templates) -# assert template_node_count == 4 -# -# concept_keys = set() -# for t in chain(sir.templates, sir_w_context.templates): -# for c in t.get_concepts(): -# concept_keys.add(c.get_key()) -# -# concept_node_count = len(concept_keys) -# assert concept_node_count == 6 -# assert 10 == template_node_count + concept_node_count -# -# # Check that all models are represented in the node lookup -# assert len(graph_data.template_nodes) == len(graph_data.template_models) -# -# # Check that the total count of nodes is as expected -# total_count = 0 -# for nodes in graph_data.template_nodes.values(): -# total_count += len(nodes) -# assert total_count == template_node_count -# -# total_count = 0 -# for nodes in graph_data.concept_nodes.values(): -# total_count += len(nodes) -# assert total_count == concept_node_count -# -# # One intra edge per concept per template per TemplateModel -# assert len(graph_data.intra_model_edges) == concept_node_count + template_node_count -# -# # (One inter edge per refinement + one inter edge per equality) per TemplateModel -# concept_equal_edges = 0 -# template_equal_edges = 0 -# concept_refinement_edges = 0 -# template_refinement_edges = 0 -# seen_concept_pairs = set() -# for t1, t2 in product(sir.templates, sir_w_context.templates): -# if t1.is_equal_to(t2, with_context=True): -# template_equal_edges += 1 -# if t1.refinement_of( -# t2, refinement_func=is_ontological_child_web, with_context=True -# ): -# template_refinement_edges += 1 -# if t2.refinement_of( -# t1, refinement_func=is_ontological_child_web, with_context=True -# ): -# template_refinement_edges += 1 -# for c1, c2 in product(t1.get_concepts(), t2.get_concepts()): -# if (c1.get_key(), c2.get_key()) in seen_concept_pairs: -# continue -# if c1.is_equal_to(c2, with_context=True): -# concept_equal_edges += 1 -# if c1.refinement_of( -# c2, refinement_func=is_ontological_child_web, with_context=True -# ): -# concept_refinement_edges += 1 -# if c2.refinement_of( -# c1, refinement_func=is_ontological_child_web, with_context=True -# ): -# concept_refinement_edges += 1 -# seen_concept_pairs.add((c1.get_key(), c2.get_key())) -# -# # Check that the counts are as expected -# assert template_equal_edges == 0 -# assert template_refinement_edges == 2 -# assert concept_equal_edges == 0 -# assert concept_refinement_edges == 3 -# -# # check that the total number of inter edges is as expected and -# # corresponds to what's in the graph data -# inter_edge_count = ( -# template_equal_edges -# + template_refinement_edges -# + concept_refinement_edges -# + concept_equal_edges -# ) -# assert inter_edge_count == 5 -# assert len(graph_data.inter_model_edges) == inter_edge_count -# -# # Score should be 0.5*number of refinement edges + 1*number of equal edges -# sim_score = graph_data.get_similarity_score(0, 1) -# assert sim_score == (0.5 * concept_refinement_edges + -# concept_equal_edges) / 3 -# assert sim_score == 1.5 / 3 +from itertools import product, chain + +from mira.examples.sir import sir +from mira.metamodel import TemplateModel, Concept +from mira.metamodel.comparison import TemplateModelComparison +from mira.dkg.web_client import is_ontological_child_web + + +def test_template_model_comp_graph_export(): + # Check counts + # Check identifications + # check expected edges + + # This will create a copy SIR model with context -> should have a + # refinement edge between each corresponding pair of nodes in the graph + sir_w_context = TemplateModel( + templates=[ + t.with_context(location="geonames:4930956") for t in sir.templates + ], + parameters=sir.parameters, + initials=sir.initials, + ) + tmc = TemplateModelComparison( + template_models=[sir, sir_w_context], + refinement_func=is_ontological_child_web, + ) + + # Check that the graph export is correct + graph_data = tmc.model_comparison + + # Check that model ids are integers and non-negative + assert len(graph_data.concept_nodes.values()) > 0 + # {model_id: {node_id: Concept|Template}} + assert isinstance(list(graph_data.template_nodes.values())[0], dict) + assert isinstance(list(list(graph_data.concept_nodes.values())[0].values())[0], + Concept) + assert list(list(graph_data.concept_nodes.values())[0].keys())[0] >= 0 + assert all(isinstance(k, int) for k in graph_data.template_models.keys()) + assert all(k >= 0 for k in graph_data.template_models.keys()) + model_id_refs = {k for k in graph_data.template_models.keys()} + + model_id_refs_nodes = {model_id for model_id in graph_data.concept_nodes.keys()} + assert model_id_refs == model_id_refs_nodes + + # One node per template per TemplateModel + one node per concept per + # template per TemplateModel + template_node_count = len(sir.templates) + len(sir_w_context.templates) + assert template_node_count == 4 + + concept_keys = set() + for t in chain(sir.templates, sir_w_context.templates): + for c in t.get_concepts(): + concept_keys.add(c.get_key()) + + concept_node_count = len(concept_keys) + assert concept_node_count == 6 + assert 10 == template_node_count + concept_node_count + + # Check that all models are represented in the node lookup + assert len(graph_data.template_nodes) == len(graph_data.template_models) + + # Check that the total count of nodes is as expected + total_count = 0 + for nodes in graph_data.template_nodes.values(): + total_count += len(nodes) + assert total_count == template_node_count + + total_count = 0 + for nodes in graph_data.concept_nodes.values(): + total_count += len(nodes) + assert total_count == concept_node_count + + # One intra edge per concept per template per TemplateModel + assert len(graph_data.intra_model_edges) == concept_node_count + template_node_count + + # (One inter edge per refinement + one inter edge per equality) per TemplateModel + concept_equal_edges = 0 + template_equal_edges = 0 + concept_refinement_edges = 0 + template_refinement_edges = 0 + seen_concept_pairs = set() + for t1, t2 in product(sir.templates, sir_w_context.templates): + if t1.is_equal_to(t2, with_context=True): + template_equal_edges += 1 + if t1.refinement_of( + t2, refinement_func=is_ontological_child_web, with_context=True + ): + template_refinement_edges += 1 + if t2.refinement_of( + t1, refinement_func=is_ontological_child_web, with_context=True + ): + template_refinement_edges += 1 + for c1, c2 in product(t1.get_concepts(), t2.get_concepts()): + if (c1.get_key(), c2.get_key()) in seen_concept_pairs: + continue + if c1.is_equal_to(c2, with_context=True): + concept_equal_edges += 1 + if c1.refinement_of( + c2, refinement_func=is_ontological_child_web, with_context=True + ): + concept_refinement_edges += 1 + if c2.refinement_of( + c1, refinement_func=is_ontological_child_web, with_context=True + ): + concept_refinement_edges += 1 + seen_concept_pairs.add((c1.get_key(), c2.get_key())) + + # Check that the counts are as expected + assert template_equal_edges == 0 + assert template_refinement_edges == 2 + assert concept_equal_edges == 0 + assert concept_refinement_edges == 3 + + # check that the total number of inter edges is as expected and + # corresponds to what's in the graph data + inter_edge_count = ( + template_equal_edges + + template_refinement_edges + + concept_refinement_edges + + concept_equal_edges + ) + assert inter_edge_count == 5 + assert len(graph_data.inter_model_edges) == inter_edge_count + + # Score should be 0.5*number of refinement edges + 1*number of equal edges + sim_score = graph_data.get_similarity_score(0, 1) + assert sim_score == (0.5 * concept_refinement_edges + + concept_equal_edges) / 3 + assert sim_score == 1.5 / 3 From fdcb602e08a2d9bf95a1345bbe7a1fb78f2006c4 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 9 Sep 2024 18:06:57 -0400 Subject: [PATCH 15/32] Override equality testing for Concepts, update deprecated json method, update validation info for Entity --- mira/dkg/client.py | 4 ++-- mira/metamodel/templates.py | 11 +++++++++++ tests/test_model_api.py | 5 +++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/mira/dkg/client.py b/mira/dkg/client.py index 1bef3ce4c..d0f1df77f 100644 --- a/mira/dkg/client.py +++ b/mira/dkg/client.py @@ -111,7 +111,7 @@ class Entity(BaseModel): link: Optional[str] = None @field_validator("link") - def set_link(cls, value, values): + def set_link(cls, value, validation_info): """ Set the value of the ``link`` field based on the value of the ``id`` field. This gets run as a post-init hook by Pydantic @@ -119,7 +119,7 @@ def set_link(cls, value, values): See also: https://stackoverflow.com/questions/54023782/pydantic-make-field-none-in-validator-based-on-other-fields-value """ - curie = values["id"] + curie = validation_info.data["id"] return f"{METAREGISTRY_BASE}/{curie}" @property diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index 2895d865d..f11c12799 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -136,6 +136,17 @@ class Concept(BaseModel): SympyExprStr: lambda e: sympy.parse_expr(e) }) + def __eq__(self, other): + if isinstance(other, Concept): + return (self.name == other.name and + self.display_name == other.display_name and + self.description == other.description and + self.identifiers == other.identifiers and + self.context == other.context and + self.units == other.units) + else: + return False + def with_context(self, do_rename=False, curie_to_name_map=None, inplace=False, **context) -> "Concept": """Return this concept with extra context. diff --git a/tests/test_model_api.py b/tests/test_model_api.py index 65e8b45a6..79ab5418d 100644 --- a/tests/test_model_api.py +++ b/tests/test_model_api.py @@ -582,12 +582,13 @@ def test_n_way_comparison_askenet(self): "exclude_defaults": True, "exclude_unset": True, "exclude_none": True, - "skip_defaults": True, + # "skip_defaults": True, } # Compare the ModelComparisonResponse models assert local_response == resp_model # If assertion fails the diff is printed local_sorted_str = sorted_json_str( - json.loads(local_response.json(**dict_options)), skip_empty=True + json.loads(local_response.model_dump_json(**dict_options)), + skip_empty=True ) resp_sorted_str = sorted_json_str( json.loads(resp_model.json(**dict_options)), skip_empty=True From d171f84438332825323a51dfde56f5f2410d0a25 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Tue, 10 Sep 2024 10:59:07 -0400 Subject: [PATCH 16/32] Add httpx as web dependency --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 1f05bf218..44b659a30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,6 +69,7 @@ metaregistry = more_click web = fastapi>=0.100.0 + httpx flask flasgger bootstrap-flask From 5a60cd69fdb57f126a2442ced192978de549bbbd Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Tue, 10 Sep 2024 11:32:44 -0400 Subject: [PATCH 17/32] Revert back old json method --- tests/test_model_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_model_api.py b/tests/test_model_api.py index 79ab5418d..b92fe907a 100644 --- a/tests/test_model_api.py +++ b/tests/test_model_api.py @@ -587,7 +587,7 @@ def test_n_way_comparison_askenet(self): # Compare the ModelComparisonResponse models assert local_response == resp_model # If assertion fails the diff is printed local_sorted_str = sorted_json_str( - json.loads(local_response.model_dump_json(**dict_options)), + json.loads(local_response.json(**dict_options)), skip_empty=True ) resp_sorted_str = sorted_json_str( From fe9c4fa9eb553f6f3e76ba84e74ea4529314cd3c Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Tue, 10 Sep 2024 13:35:36 -0400 Subject: [PATCH 18/32] Regenerate schema after PR for parameter distrubtion expressions have been merged and cast sympy.Float to float for serialization --- mira/metamodel/ops.py | 4 +- mira/metamodel/schema.json | 2348 ++++++++++++++++++++++-------------- 2 files changed, 1437 insertions(+), 915 deletions(-) diff --git a/mira/metamodel/ops.py b/mira/metamodel/ops.py index a4b209d43..adec6c751 100644 --- a/mira/metamodel/ops.py +++ b/mira/metamodel/ops.py @@ -725,7 +725,9 @@ def counts_to_dimensionless(tm: TemplateModel, SympyExprStr(p.units.expression.args[0] / (counts_unit_symbol ** exponent)) p.value /= (norm_factor ** exponent) - + # Previously was sympy.Float object, cannot be serialized in + # Pydantic2 type enforcement + p.value = float(p.value) return tm diff --git a/mira/metamodel/schema.json b/mira/metamodel/schema.json index e15eb25df..a09b4feb0 100644 --- a/mira/metamodel/schema.json +++ b/mira/metamodel/schema.json @@ -3,1415 +3,1935 @@ "$id": "https://raw.githubusercontent.com/indralab/mira/main/mira/metamodel/schema.json", "title": "MIRA Metamodel Template Schema", "description": "MIRA metamodel templates give a high-level abstraction of modeling appropriate for many domains.", - "definitions": { - "Unit": { - "title": "Unit", - "description": "A unit of measurement.", - "type": "object", + "$defs": { + "Annotations": { + "description": "A metadata model for model-level annotations.\n\nExamples in this metadata model are taken from\nhttps://www.ebi.ac.uk/biomodels/BIOMD0000000956,\na well-annotated SIR model in the BioModels database.", "properties": { - "expression": { - "title": "Expression", - "description": "The expression for the unit.", - "type": "string", - "example": "2*x" + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A human-readable label for the model", + "examples": [ + "SIR model of scenarios of COVID-19 spread in CA and NY" + ], + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A description of the model", + "examples": [ + "The coronavirus disease 2019 (COVID-19) pandemic has placed epidemic modeling at the forefront of worldwide public policy making. Nonetheless, modeling and forecasting the spread of COVID-19 remains a challenge. Here, we detail three regional scale models for forecasting and assessing the course of the pandemic. This work demonstrates the utility of parsimonious models for early-time data and provides an accessible framework for generating policy-relevant insights into its course. We show how these models can be connected to each other and to time series data for a particular region. Capable of measuring and forecasting the impacts of social distancing, these models highlight the dangers of relaxing nonpharmaceutical public health interventions in the absence of a vaccine or antiviral therapies." + ], + "title": "Description" + }, + "license": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Information about the licensing of the model artifact. Ideally, given as an SPDX identifier like CC0 or CC-BY-4.0. For example, models from the BioModels databases are all licensed under the CC0 public attribution license.", + "examples": [ + "CC0" + ], + "title": "License" + }, + "authors": { + "description": "A list of authors/creators of the model. This is not the same as the people who e.g., submitted the model to BioModels", + "examples": [ + [ + { + "name": "Andrea L Bertozzi" + }, + { + "name": "Elisa Franco" + }, + { + "name": "George Mohler" + }, + { + "name": "Martin B Short" + }, + { + "name": "Daniel Sledge" + } + ] + ], + "items": { + "$ref": "#/$defs/Author" + }, + "title": "Authors", + "type": "array" + }, + "references": { + "description": "A list of CURIEs (i.e., :) corresponding to literature references that describe the model. Do **not** duplicate the same publication with different CURIEs (e.g., using pubmed, pmc, and doi)", + "examples": [ + [ + "pubmed:32616574" + ] + ], + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "time_scale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The granularity of the time element of the model, typically on the scale of days, weeks, or months for epidemiology models", + "examples": [ + "day" + ], + "title": "Time Scale" + }, + "time_start": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The start time of the applicability of a model, given as a datetime. When the time scale is not so granular, leave the less granular fields as default, i.e., if the time scale is on months, give dates like YYYY-MM-01 00:00", + "title": "Time Start" + }, + "time_end": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Similar to the start time of the applicability of a model, the end time is given as a datetime. For example, the Bertozzi 2020 model is applicable between March and August 2020, so this field is annotated with August 1st, 2020.", + "title": "Time End" + }, + "locations": { + "description": "A location or list of locations where this model is applicable, ideally annotated using a CURIEs referencing a controlled vocabulary such as GeoNames, which has multiple levels of granularity including city/state/country level terms. For example,the Bertozzi 2020 model was for New York City (geonames:5128581) and California (geonames:5332921)", + "examples": [ + [ + "geonames:5128581", + "geonames:5332921" + ] + ], + "items": { + "type": "string" + }, + "title": "Locations", + "type": "array" + }, + "pathogens": { + "description": "The pathogens present in the model, given with CURIEs referencing vocabulary for taxa, ideally, NCBI Taxonomy. For example, the Bertozzi 2020 model is about SARS-CoV-2, this is ncbitaxon:2697049. Do not confuse this field with terms for annotating the disease caused by the pathogen. Note that some models may have multiple pathogens, for simulating double pandemics such as the interaction with SARS-CoV-2 and the seasonal flu.", + "examples": [ + [ + "ncbitaxon:2697049" + ] + ], + "items": { + "type": "string" + }, + "title": "Pathogens", + "type": "array" + }, + "diseases": { + "description": "The diseases caused by pathogens in the model, given with CURIEs referencing vocabulary for dieases, such as DOID, EFO, or MONDO. For example, the Bertozzi 2020 model is about SARS-CoV-2, which causes COVID-19. In the Human Disease Ontology (DOID), this is referenced by doid:0080600.", + "examples": [ + [ + "doid:0080600" + ] + ], + "items": { + "type": "string" + }, + "title": "Diseases", + "type": "array" + }, + "hosts": { + "description": "The hosts present in the model, given with CURIEs referencing vocabulary for taxa, ideally, NCBI Taxonomy. For example, the Bertozzi 2020 model is about human infection by SARS-CoV-2. Therefore, the appropriate annotation for this field would be ncbitaxon:9606. Note that some models have multiple hosts, such as Malaria models that consider humans and mosquitos.", + "examples": [ + [ + "ncbitaxon:9606" + ] + ], + "items": { + "type": "string" + }, + "title": "Hosts", + "type": "array" + }, + "model_types": { + "description": "This field describes the type(s) of the model using the Mathematical Modeling Ontology (MAMO), which has terms like 'ordinary differential equation model', 'population model', etc. These should be annotated as CURIEs in the form of mamo:. For example, the Bertozzi 2020 model is a population model (mamo:0000028) and ordinary differential equation model (mamo:0000046)", + "examples": [ + [ + "mamo:0000028", + "mamo:0000046" + ] + ], + "items": { + "type": "string" + }, + "title": "Model Types", + "type": "array" + } + }, + "title": "Annotations", + "type": "object" + }, + "Author": { + "description": "A metadata model for an author.", + "properties": { + "name": { + "description": "The name of the author", + "title": "Name", + "type": "string" } }, "required": [ - "expression" - ] + "name" + ], + "title": "Author", + "type": "object" }, "Concept": { - "title": "Concept", "description": "A concept is specified by its identifier(s), name, and - optionally -\nits context.", - "type": "object", "properties": { "name": { - "title": "Name", "description": "The name of the concept.", + "title": "Name", "type": "string" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "An optional display name for the concept. If not provided, the name can be used for display purposes.", - "type": "string" + "title": "Display Name" }, "description": { - "title": "Description", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "An optional description of the concept.", - "type": "string" + "title": "Description" }, "identifiers": { - "title": "Identifiers", - "description": "A mapping of namespaces to identifiers.", - "type": "object", "additionalProperties": { "type": "string" - } + }, + "description": "A mapping of namespaces to identifiers.", + "title": "Identifiers", + "type": "object" }, "context": { - "title": "Context", - "description": "A mapping of context keys to values.", - "type": "object", "additionalProperties": { - "type": "string" - } + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "description": "A mapping of context keys to values.", + "title": "Context", + "type": "object" }, "units": { - "title": "Units", - "description": "The units of the concept.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/$defs/Unit" + }, { - "$ref": "#/definitions/Unit" + "type": "null" } - ] + ], + "default": null, + "description": "The units of the concept." } }, "required": [ "name" - ] - }, - "Template": { - "title": "Template", - "description": "The Template is a parent class for model processes", - "type": "object", - "properties": { - "rate_law": { - "title": "Rate Law", - "description": "The rate law for the template.", - "type": "string", - "example": "2*x" - }, - "name": { - "title": "Name", - "description": "The name of the template.", - "type": "string" - }, - "display_name": { - "title": "Display Name", - "description": "The display name of the template.", - "type": "string" - } - } - }, - "Provenance": { - "title": "Provenance", - "type": "object", - "properties": {} + ], + "title": "Concept", + "type": "object" }, "ControlledConversion": { - "title": "ControlledConversion", "description": "Specifies a process of controlled conversion from subject to outcome,\ncontrolled by the controller.", - "type": "object", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "ControlledConversion", "const": "ControlledConversion", + "default": "ControlledConversion", "enum": [ "ControlledConversion" ], + "title": "Type", "type": "string" }, "controller": { - "title": "Controller", - "description": "The controller of the conversion.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The controller of the conversion." }, "subject": { - "title": "Subject", - "description": "The subject of the conversion.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The subject of the conversion." }, "outcome": { - "title": "Outcome", - "description": "The outcome of the conversion.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The outcome of the conversion." }, "provenance": { - "title": "Provenance", "description": "The provenance of the conversion.", - "type": "array", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ "controller", "subject", "outcome" - ] + ], + "title": "ControlledConversion", + "type": "object" }, - "GroupedControlledConversion": { - "title": "GroupedControlledConversion", - "description": "The Template is a parent class for model processes", - "type": "object", + "ControlledDegradation": { + "description": "Specifies a process of degradation controlled by one controller", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "GroupedControlledConversion", - "const": "GroupedControlledConversion", + "const": "ControlledDegradation", + "default": "ControlledDegradation", "enum": [ - "GroupedControlledConversion" + "ControlledDegradation" ], + "title": "Type", "type": "string" }, - "controllers": { - "title": "Controllers", - "description": "The controllers of the conversion.", - "type": "array", - "items": { - "$ref": "#/definitions/Concept" - } + "controller": { + "$ref": "#/$defs/Concept", + "description": "The controller of the degradation." }, "subject": { - "title": "Subject", - "description": "The subject of the conversion.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] - }, - "outcome": { - "title": "Outcome", - "description": "The outcome of the conversion.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The subject of the degradation." }, "provenance": { - "title": "Provenance", - "description": "The provenance of the conversion.", - "type": "array", + "description": "The provenance of the degradation.", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ - "controllers", - "subject", - "outcome" - ] + "controller", + "subject" + ], + "title": "ControlledDegradation", + "type": "object" }, - "GroupedControlledProduction": { - "title": "GroupedControlledProduction", - "description": "Specifies a process of production controlled by several controllers", - "type": "object", + "ControlledProduction": { + "description": "Specifies a process of production controlled by one controller", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "GroupedControlledProduction", - "const": "GroupedControlledProduction", + "const": "ControlledProduction", + "default": "ControlledProduction", "enum": [ - "GroupedControlledProduction" + "ControlledProduction" ], + "title": "Type", "type": "string" }, - "controllers": { - "title": "Controllers", - "description": "The controllers of the production.", - "type": "array", - "items": { - "$ref": "#/definitions/Concept" - } + "controller": { + "$ref": "#/$defs/Concept", + "description": "The controller of the production." }, "outcome": { - "title": "Outcome", - "description": "The outcome of the production.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The outcome of the production." }, "provenance": { - "title": "Provenance", - "description": "The provenance of the production.", - "type": "array", + "description": "Provenance of the template", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ - "controllers", + "controller", "outcome" - ] - }, - "ControlledProduction": { + ], "title": "ControlledProduction", - "description": "Specifies a process of production controlled by one controller", - "type": "object", + "type": "object" + }, + "ControlledReplication": { + "description": "Specifies a process of replication controlled by one controller", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "ControlledProduction", - "const": "ControlledProduction", + "const": "ControlledReplication", + "default": "ControlledReplication", "enum": [ - "ControlledProduction" + "ControlledReplication" ], + "title": "Type", "type": "string" }, "controller": { - "title": "Controller", - "description": "The controller of the production.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The controller of the replication." }, - "outcome": { - "title": "Outcome", - "description": "The outcome of the production.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "subject": { + "$ref": "#/$defs/Concept", + "description": "The subject of the replication." }, "provenance": { - "title": "Provenance", - "description": "Provenance of the template", - "type": "array", + "description": "The provenance of the replication.", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ "controller", - "outcome" - ] + "subject" + ], + "title": "ControlledReplication", + "type": "object" }, - "NaturalConversion": { - "title": "NaturalConversion", - "description": "Specifies a process of natural conversion from subject to outcome", - "type": "object", + "Distribution": { + "description": "A distribution of values for a parameter.", "properties": { - "rate_law": { - "title": "Rate Law", - "description": "The rate law for the template.", - "type": "string", - "example": "2*x" - }, - "name": { - "title": "Name", - "description": "The name of the template.", - "type": "string" - }, - "display_name": { - "title": "Display Name", - "description": "The display name of the template.", - "type": "string" - }, "type": { + "description": "The type of distribution as provided by ProbOnto e.g. 'StandardUniform1', 'Beta1', etc.", "title": "Type", - "default": "NaturalConversion", - "const": "NaturalConversion", - "enum": [ - "NaturalConversion" - ], "type": "string" }, - "subject": { - "title": "Subject", - "description": "The subject of the conversion.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] - }, - "outcome": { - "title": "Outcome", - "description": "The outcome of the conversion.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] - }, - "provenance": { - "title": "Provenance", - "description": "The provenance of the conversion.", - "type": "array", - "items": { - "$ref": "#/definitions/Provenance" - } + "parameters": { + "additionalProperties": { + "anyOf": [ + { + "type": "number" + }, + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + } + ] + }, + "description": "The parameters of the distribution keyed by parameter names controlled by ProbOnto and values that are either floating point values or symbolic expressions over other parameters.", + "title": "Parameters", + "type": "object" } }, "required": [ - "subject", - "outcome" - ] + "type", + "parameters" + ], + "title": "Distribution", + "type": "object" }, - "MultiConversion": { - "title": "MultiConversion", - "description": "Specifies a conversion process of multiple subjects and outcomes.", - "type": "object", + "GroupedControlledConversion": { "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "MultiConversion", - "const": "MultiConversion", + "const": "GroupedControlledConversion", + "default": "GroupedControlledConversion", "enum": [ - "MultiConversion" + "GroupedControlledConversion" ], + "title": "Type", "type": "string" }, - "subjects": { - "title": "Subjects", - "description": "The subjects of the conversion.", - "type": "array", + "controllers": { + "description": "The controllers of the conversion.", "items": { - "$ref": "#/definitions/Concept" - } + "$ref": "#/$defs/Concept" + }, + "title": "Controllers", + "type": "array" }, - "outcomes": { - "title": "Outcomes", - "description": "The outcomes of the conversion.", - "type": "array", - "items": { - "$ref": "#/definitions/Concept" - } + "subject": { + "$ref": "#/$defs/Concept", + "description": "The subject of the conversion." + }, + "outcome": { + "$ref": "#/$defs/Concept", + "description": "The outcome of the conversion." }, "provenance": { - "title": "Provenance", "description": "The provenance of the conversion.", - "type": "array", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ - "subjects", - "outcomes" - ] + "controllers", + "subject", + "outcome" + ], + "title": "GroupedControlledConversion", + "type": "object" }, - "ReversibleFlux": { - "title": "ReversibleFlux", - "description": "Specifies a reversible flux between a left and right side.", - "type": "object", + "GroupedControlledDegradation": { + "description": "Specifies a process of degradation controlled by several controllers", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "ReversibleFlux", - "const": "ReversibleFlux", + "const": "GroupedControlledDegradation", + "default": "GroupedControlledDegradation", "enum": [ - "ReversibleFlux" + "GroupedControlledDegradation" ], + "title": "Type", "type": "string" }, - "left": { - "title": "Left", - "description": "The left hand side of the flux.", - "type": "array", + "controllers": { + "description": "The controllers of the degradation.", "items": { - "$ref": "#/definitions/Concept" - } + "$ref": "#/$defs/Concept" + }, + "title": "Controllers", + "type": "array" }, - "right": { - "title": "Right", - "description": "The right hand side of the flux.", - "type": "array", - "items": { - "$ref": "#/definitions/Concept" - } + "subject": { + "$ref": "#/$defs/Concept", + "description": "The subject of the degradation." }, "provenance": { - "title": "Provenance", - "description": "The provenance of the flux.", - "type": "array", + "description": "The provenance of the degradation.", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ - "left", - "right" - ] + "controllers", + "subject" + ], + "title": "GroupedControlledDegradation", + "type": "object" }, - "NaturalProduction": { - "title": "NaturalProduction", - "description": "A template for the production of a species at a constant rate.", - "type": "object", + "GroupedControlledProduction": { + "description": "Specifies a process of production controlled by several controllers", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "NaturalProduction", - "const": "NaturalProduction", + "const": "GroupedControlledProduction", + "default": "GroupedControlledProduction", "enum": [ - "NaturalProduction" + "GroupedControlledProduction" ], + "title": "Type", "type": "string" }, + "controllers": { + "description": "The controllers of the production.", + "items": { + "$ref": "#/$defs/Concept" + }, + "title": "Controllers", + "type": "array" + }, "outcome": { - "title": "Outcome", - "description": "The outcome of the production.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The outcome of the production." }, "provenance": { - "title": "Provenance", "description": "The provenance of the production.", - "type": "array", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ + "controllers", "outcome" - ] + ], + "title": "GroupedControlledProduction", + "type": "object" }, - "NaturalDegradation": { - "title": "NaturalDegradation", - "description": "A template for the degradataion of a species at a proportional rate to its amount.", - "type": "object", + "Initial": { + "description": "Represents the initial conditions for parameters present in the\nmodel.", "properties": { - "rate_law": { - "title": "Rate Law", - "description": "The rate law for the template.", - "type": "string", - "example": "2*x" - }, - "name": { - "title": "Name", - "description": "The name of the template.", - "type": "string" - }, - "display_name": { - "title": "Display Name", - "description": "The display name of the template.", - "type": "string" + "concept": { + "$ref": "#/$defs/Concept", + "description": "The concept associated with the initial." }, - "type": { - "title": "Type", - "default": "NaturalDegradation", - "const": "NaturalDegradation", - "enum": [ - "NaturalDegradation" - ], + "expression": { + "anyOf": [], + "description": "The expression for the initial.", + "format": "sympy-expr", + "title": "Expression", "type": "string" - }, - "subject": { - "title": "Subject", - "description": "The subject of the degradation.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] - }, - "provenance": { - "title": "Provenance", - "description": "The provenance of the degradation.", - "type": "array", - "items": { - "$ref": "#/definitions/Provenance" - } } }, "required": [ - "subject" - ] + "concept", + "expression" + ], + "title": "Initial", + "type": "object" }, - "ControlledDegradation": { - "title": "ControlledDegradation", - "description": "Specifies a process of degradation controlled by one controller", - "type": "object", + "MultiConversion": { + "description": "Specifies a conversion process of multiple subjects and outcomes.", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "ControlledDegradation", - "const": "ControlledDegradation", + "const": "MultiConversion", + "default": "MultiConversion", "enum": [ - "ControlledDegradation" + "MultiConversion" ], + "title": "Type", "type": "string" }, - "controller": { - "title": "Controller", - "description": "The controller of the degradation.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "subjects": { + "description": "The subjects of the conversion.", + "items": { + "$ref": "#/$defs/Concept" + }, + "title": "Subjects", + "type": "array" }, - "subject": { - "title": "Subject", - "description": "The subject of the degradation.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "outcomes": { + "description": "The outcomes of the conversion.", + "items": { + "$ref": "#/$defs/Concept" + }, + "title": "Outcomes", + "type": "array" }, "provenance": { - "title": "Provenance", - "description": "The provenance of the degradation.", - "type": "array", + "description": "The provenance of the conversion.", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ - "controller", - "subject" - ] + "subjects", + "outcomes" + ], + "title": "MultiConversion", + "type": "object" }, - "GroupedControlledDegradation": { - "title": "GroupedControlledDegradation", - "description": "Specifies a process of degradation controlled by several controllers", - "type": "object", + "NaturalConversion": { + "description": "Specifies a process of natural conversion from subject to outcome", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "GroupedControlledDegradation", - "const": "GroupedControlledDegradation", + "const": "NaturalConversion", + "default": "NaturalConversion", "enum": [ - "GroupedControlledDegradation" + "NaturalConversion" ], + "title": "Type", "type": "string" }, - "controllers": { - "title": "Controllers", - "description": "The controllers of the degradation.", - "type": "array", - "items": { - "$ref": "#/definitions/Concept" - } - }, "subject": { - "title": "Subject", - "description": "The subject of the degradation.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The subject of the conversion." + }, + "outcome": { + "$ref": "#/$defs/Concept", + "description": "The outcome of the conversion." }, "provenance": { - "title": "Provenance", - "description": "The provenance of the degradation.", - "type": "array", + "description": "The provenance of the conversion.", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ - "controllers", - "subject" - ] + "subject", + "outcome" + ], + "title": "NaturalConversion", + "type": "object" }, - "NaturalReplication": { - "title": "NaturalReplication", - "description": "Specifies a process of natural replication of a subject.", - "type": "object", + "NaturalDegradation": { + "description": "A template for the degradataion of a species at a proportional rate to its amount.", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "NaturalReplication", - "const": "NaturalReplication", + "const": "NaturalDegradation", + "default": "NaturalDegradation", "enum": [ - "NaturalReplication" + "NaturalDegradation" ], + "title": "Type", "type": "string" }, "subject": { - "title": "Subject", - "description": "The subject of the replication.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The subject of the degradation." }, "provenance": { - "title": "Provenance", - "description": "The provenance of the template.", - "type": "array", + "description": "The provenance of the degradation.", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ "subject" - ] + ], + "title": "NaturalDegradation", + "type": "object" }, - "ControlledReplication": { - "title": "ControlledReplication", - "description": "Specifies a process of replication controlled by one controller", - "type": "object", + "NaturalProduction": { + "description": "A template for the production of a species at a constant rate.", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "ControlledReplication", - "const": "ControlledReplication", + "const": "NaturalProduction", + "default": "NaturalProduction", "enum": [ - "ControlledReplication" + "NaturalProduction" ], + "title": "Type", "type": "string" }, - "controller": { - "title": "Controller", - "description": "The controller of the replication.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] - }, - "subject": { - "title": "Subject", - "description": "The subject of the replication.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "outcome": { + "$ref": "#/$defs/Concept", + "description": "The outcome of the production." }, "provenance": { - "title": "Provenance", - "description": "The provenance of the replication.", - "type": "array", + "description": "The provenance of the production.", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ - "controller", - "subject" - ] + "outcome" + ], + "title": "NaturalProduction", + "type": "object" }, - "StaticConcept": { - "title": "StaticConcept", - "description": "Specifies a standalone Concept that is not part of a process.", - "type": "object", + "NaturalReplication": { + "description": "Specifies a process of natural replication of a subject.", "properties": { "rate_law": { - "title": "Rate Law", + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The rate law for the template.", - "type": "string", - "example": "2*x" + "title": "Rate Law" }, "name": { - "title": "Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The name of the template.", - "type": "string" + "title": "Name" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "The display name of the template.", - "type": "string" + "title": "Display Name" }, "type": { - "title": "Type", - "default": "StaticConcept", - "const": "StaticConcept", + "const": "NaturalReplication", + "default": "NaturalReplication", "enum": [ - "StaticConcept" + "NaturalReplication" ], + "title": "Type", "type": "string" }, "subject": { - "title": "Subject", - "description": "The subject.", - "allOf": [ - { - "$ref": "#/definitions/Concept" - } - ] + "$ref": "#/$defs/Concept", + "description": "The subject of the replication." }, "provenance": { - "title": "Provenance", - "description": "The provenance.", - "type": "array", + "description": "The provenance of the template.", "items": { - "$ref": "#/definitions/Provenance" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } }, "required": [ "subject" - ] - }, - "Distribution": { - "title": "Distribution", - "description": "A distribution of values for a parameter.", - "type": "object", - "properties": { - "type": { - "title": "Type", - "description": "The type of distribution as provided by ProbOnto e.g. 'StandardUniform1', 'Beta1', etc.", - "type": "string" - }, - "parameters": { - "title": "Parameters", - "description": "The parameters of the distribution keyed by parameter names controlled by ProbOnto and values that are either floating point values or symbolic expressions over other parameters.", - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "example": "2*x" - } - ] - } - } - }, - "required": [ - "type", - "parameters" - ] + ], + "title": "NaturalReplication", + "type": "object" }, - "Parameter": { - "title": "Parameter", - "description": "A Parameter is a special type of Concept that carries a value.", - "type": "object", + "Observable": { + "description": "An observable is a special type of Concept that carries an expression.\n\nObservables are used to define the readouts of a model, useful when a\nreadout is not defined as a state variable but is rather a function of\nstate variables.", "properties": { "name": { - "title": "Name", "description": "The name of the concept.", + "title": "Name", "type": "string" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "An optional display name for the concept. If not provided, the name can be used for display purposes.", - "type": "string" + "title": "Display Name" }, "description": { - "title": "Description", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "An optional description of the concept.", - "type": "string" + "title": "Description" }, "identifiers": { - "title": "Identifiers", - "description": "A mapping of namespaces to identifiers.", - "type": "object", "additionalProperties": { "type": "string" - } + }, + "description": "A mapping of namespaces to identifiers.", + "title": "Identifiers", + "type": "object" }, "context": { - "title": "Context", - "description": "A mapping of context keys to values.", - "type": "object", "additionalProperties": { - "type": "string" - } + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "description": "A mapping of context keys to values.", + "title": "Context", + "type": "object" }, "units": { - "title": "Units", - "description": "The units of the concept.", - "allOf": [ - { - "$ref": "#/definitions/Unit" - } - ] - }, - "value": { - "title": "Value", - "description": "Value of the parameter.", - "type": "number" - }, - "distribution": { - "title": "Distribution", - "description": "A distribution of values for the parameter.", - "allOf": [ + "anyOf": [ { - "$ref": "#/definitions/Distribution" - } - ] - } - }, - "required": [ - "name" - ] - }, - "Initial": { - "title": "Initial", - "description": "Represents the initial conditions for parameters present in the\nmodel.", - "type": "object", - "properties": { - "concept": { - "title": "Concept", - "description": "The concept associated with the initial.", - "allOf": [ + "$ref": "#/$defs/Unit" + }, { - "$ref": "#/definitions/Concept" + "type": "null" } - ] + ], + "default": null, + "description": "The units of the concept." }, "expression": { + "anyOf": [], + "description": "The expression for the observable.", + "format": "sympy-expr", "title": "Expression", - "description": "The expression for the initial.", - "type": "string", - "example": "2*x" + "type": "string" } }, "required": [ - "concept", + "name", "expression" - ] - }, - "Observable": { + ], "title": "Observable", - "description": "An observable is a special type of Concept that carries an expression.\n\nObservables are used to define the readouts of a model, useful when a\nreadout is not defined as a state variable but is rather a function of\nstate variables.", - "type": "object", + "type": "object" + }, + "Parameter": { + "description": "A Parameter is a special type of Concept that carries a value.", "properties": { "name": { - "title": "Name", "description": "The name of the concept.", + "title": "Name", "type": "string" }, "display_name": { - "title": "Display Name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "An optional display name for the concept. If not provided, the name can be used for display purposes.", - "type": "string" + "title": "Display Name" }, "description": { - "title": "Description", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, "description": "An optional description of the concept.", - "type": "string" + "title": "Description" }, "identifiers": { - "title": "Identifiers", - "description": "A mapping of namespaces to identifiers.", - "type": "object", "additionalProperties": { "type": "string" - } + }, + "description": "A mapping of namespaces to identifiers.", + "title": "Identifiers", + "type": "object" }, "context": { - "title": "Context", - "description": "A mapping of context keys to values.", - "type": "object", "additionalProperties": { - "type": "string" - } + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "description": "A mapping of context keys to values.", + "title": "Context", + "type": "object" }, "units": { - "title": "Units", - "description": "The units of the concept.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/$defs/Unit" + }, { - "$ref": "#/definitions/Unit" + "type": "null" } - ] - }, - "expression": { - "title": "Expression", - "description": "The expression for the observable.", - "type": "string", - "example": "2*x" - } - }, - "required": [ - "name", - "expression" - ] - }, - "Author": { - "title": "Author", - "description": "A metadata model for an author.", - "type": "object", - "properties": { - "name": { - "title": "Name", - "description": "The name of the author", - "type": "string" - } - }, - "required": [ - "name" - ] - }, - "Annotations": { - "title": "Annotations", - "description": "A metadata model for model-level annotations.\n\nExamples in this metadata model are taken from\nhttps://www.ebi.ac.uk/biomodels/BIOMD0000000956,\na well-annotated SIR model in the BioModels database.", - "type": "object", - "properties": { - "name": { - "title": "Name", - "description": "A human-readable label for the model", - "example": "SIR model of scenarios of COVID-19 spread in CA and NY", - "type": "string" - }, - "description": { - "title": "Description", - "description": "A description of the model", - "example": "The coronavirus disease 2019 (COVID-19) pandemic has placed epidemic modeling at the forefront of worldwide public policy making. Nonetheless, modeling and forecasting the spread of COVID-19 remains a challenge. Here, we detail three regional scale models for forecasting and assessing the course of the pandemic. This work demonstrates the utility of parsimonious models for early-time data and provides an accessible framework for generating policy-relevant insights into its course. We show how these models can be connected to each other and to time series data for a particular region. Capable of measuring and forecasting the impacts of social distancing, these models highlight the dangers of relaxing nonpharmaceutical public health interventions in the absence of a vaccine or antiviral therapies.", - "type": "string" - }, - "license": { - "title": "License", - "description": "Information about the licensing of the model artifact. Ideally, given as an SPDX identifier like CC0 or CC-BY-4.0. For example, models from the BioModels databases are all licensed under the CC0 public attribution license.", - "example": "CC0", - "type": "string" + ], + "default": null, + "description": "The units of the concept." }, - "authors": { - "title": "Authors", - "description": "A list of authors/creators of the model. This is not the same as the people who e.g., submitted the model to BioModels", - "example": [ + "value": { + "anyOf": [ { - "name": "Andrea L Bertozzi" + "type": "number" }, { - "name": "Elisa Franco" + "type": "null" + } + ], + "default": null, + "description": "Value of the parameter.", + "title": "Value" + }, + "distribution": { + "anyOf": [ + { + "$ref": "#/$defs/Distribution" }, { - "name": "George Mohler" + "type": "null" + } + ], + "default": null, + "description": "A distribution of values for the parameter." + } + }, + "required": [ + "name" + ], + "title": "Parameter", + "type": "object" + }, + "Provenance": { + "properties": {}, + "title": "Provenance", + "type": "object" + }, + "ReversibleFlux": { + "description": "Specifies a reversible flux between a left and right side.", + "properties": { + "rate_law": { + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" }, { - "name": "Martin B Short" + "type": "null" + } + ], + "default": null, + "description": "The rate law for the template.", + "title": "Rate Law" + }, + "name": { + "anyOf": [ + { + "type": "string" }, { - "name": "Daniel Sledge" + "type": "null" } ], - "type": "array", - "items": { - "$ref": "#/definitions/Author" - } + "default": null, + "description": "The name of the template.", + "title": "Name" }, - "references": { - "title": "References", - "description": "A list of CURIEs (i.e., :) corresponding to literature references that describe the model. Do **not** duplicate the same publication with different CURIEs (e.g., using pubmed, pmc, and doi)", - "example": [ - "pubmed:32616574" + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ], - "type": "array", - "items": { - "type": "string" - } + "default": null, + "description": "The display name of the template.", + "title": "Display Name" }, - "time_scale": { - "title": "Time Scale", - "description": "The granularity of the time element of the model, typically on the scale of days, weeks, or months for epidemiology models", - "example": "day", + "type": { + "const": "ReversibleFlux", + "default": "ReversibleFlux", + "enum": [ + "ReversibleFlux" + ], + "title": "Type", "type": "string" }, - "time_start": { - "title": "Time Start", - "description": "The start time of the applicability of a model, given as a datetime. When the time scale is not so granular, leave the less granular fields as default, i.e., if the time scale is on months, give dates like YYYY-MM-01 00:00", - "type": "string", - "format": "date-time" - }, - "time_end": { - "title": "Time End", - "description": "Similar to the start time of the applicability of a model, the end time is given as a datetime. For example, the Bertozzi 2020 model is applicable between March and August 2020, so this field is annotated with August 1st, 2020.", - "type": "string", - "format": "date-time" + "left": { + "description": "The left hand side of the flux.", + "items": { + "$ref": "#/$defs/Concept" + }, + "title": "Left", + "type": "array" }, - "locations": { - "title": "Locations", - "description": "A location or list of locations where this model is applicable, ideally annotated using a CURIEs referencing a controlled vocabulary such as GeoNames, which has multiple levels of granularity including city/state/country level terms. For example,the Bertozzi 2020 model was for New York City (geonames:5128581) and California (geonames:5332921)", - "example": [ - "geonames:5128581", - "geonames:5332921" - ], - "type": "array", + "right": { + "description": "The right hand side of the flux.", "items": { - "type": "string" - } + "$ref": "#/$defs/Concept" + }, + "title": "Right", + "type": "array" }, - "pathogens": { - "title": "Pathogens", - "description": "The pathogens present in the model, given with CURIEs referencing vocabulary for taxa, ideally, NCBI Taxonomy. For example, the Bertozzi 2020 model is about SARS-CoV-2, this is ncbitaxon:2697049. Do not confuse this field with terms for annotating the disease caused by the pathogen. Note that some models may have multiple pathogens, for simulating double pandemics such as the interaction with SARS-CoV-2 and the seasonal flu.", - "example": [ - "ncbitaxon:2697049" - ], - "type": "array", + "provenance": { + "description": "The provenance of the flux.", "items": { - "type": "string" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" + } + }, + "required": [ + "left", + "right" + ], + "title": "ReversibleFlux", + "type": "object" + }, + "StaticConcept": { + "description": "Specifies a standalone Concept that is not part of a process.", + "properties": { + "rate_law": { + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The rate law for the template.", + "title": "Rate Law" }, - "diseases": { - "title": "Diseases", - "description": "The diseases caused by pathogens in the model, given with CURIEs referencing vocabulary for dieases, such as DOID, EFO, or MONDO. For example, the Bertozzi 2020 model is about SARS-CoV-2, which causes COVID-19. In the Human Disease Ontology (DOID), this is referenced by doid:0080600.", - "example": [ - "doid:0080600" + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ], - "type": "array", - "items": { - "type": "string" - } + "default": null, + "description": "The name of the template.", + "title": "Name" }, - "hosts": { - "title": "Hosts", - "description": "The hosts present in the model, given with CURIEs referencing vocabulary for taxa, ideally, NCBI Taxonomy. For example, the Bertozzi 2020 model is about human infection by SARS-CoV-2. Therefore, the appropriate annotation for this field would be ncbitaxon:9606. Note that some models have multiple hosts, such as Malaria models that consider humans and mosquitos.", - "example": [ - "ncbitaxon:9606" + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ], - "type": "array", - "items": { - "type": "string" - } + "default": null, + "description": "The display name of the template.", + "title": "Display Name" }, - "model_types": { - "title": "Model Types", - "description": "This field describes the type(s) of the model using the Mathematical Modeling Ontology (MAMO), which has terms like 'ordinary differential equation model', 'population model', etc. These should be annotated as CURIEs in the form of mamo:. For example, the Bertozzi 2020 model is a population model (mamo:0000028) and ordinary differential equation model (mamo:0000046)", - "example": [ - "mamo:0000028", - "mamo:0000046" + "type": { + "const": "StaticConcept", + "default": "StaticConcept", + "enum": [ + "StaticConcept" ], - "type": "array", + "title": "Type", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/Concept", + "description": "The subject." + }, + "provenance": { + "description": "The provenance.", "items": { - "type": "string" - } + "$ref": "#/$defs/Provenance" + }, + "title": "Provenance", + "type": "array" } - } + }, + "required": [ + "subject" + ], + "title": "StaticConcept", + "type": "object" }, - "Time": { - "title": "Time", - "description": "A special type of Concept that represents time.", - "type": "object", + "Template": { + "description": "The Template is a parent class for model processes", "properties": { + "rate_law": { + "anyOf": [ + { + "anyOf": [], + "format": "sympy-expr", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The rate law for the template.", + "title": "Rate Law" + }, "name": { - "title": "Name", - "description": "The symbol of the time variable in the model.", - "default": "t", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The name of the template.", + "title": "Name" }, - "units": { - "title": "Units", - "description": "The units of the time variable.", - "allOf": [ + "display_name": { + "anyOf": [ { - "$ref": "#/definitions/Unit" + "type": "string" + }, + { + "type": "null" } - ] + ], + "default": null, + "description": "The display name of the template.", + "title": "Display Name" } - } + }, + "title": "Template", + "type": "object" }, "TemplateModel": { - "title": "TemplateModel", "description": "A template model.", - "type": "object", "properties": { "templates": { - "title": "Templates", "description": "A list of any child class of Templates", - "type": "array", "items": { + "description": "Any child class of a Template", "discriminator": { - "propertyName": "type", "mapping": { - "NaturalConversion": "#/definitions/NaturalConversion", - "MultiConversion": "#/definitions/MultiConversion", - "ControlledConversion": "#/definitions/ControlledConversion", - "NaturalDegradation": "#/definitions/NaturalDegradation", - "ControlledDegradation": "#/definitions/ControlledDegradation", - "GroupedControlledDegradation": "#/definitions/GroupedControlledDegradation", - "NaturalProduction": "#/definitions/NaturalProduction", - "ControlledProduction": "#/definitions/ControlledProduction", - "GroupedControlledConversion": "#/definitions/GroupedControlledConversion", - "GroupedControlledProduction": "#/definitions/GroupedControlledProduction", - "NaturalReplication": "#/definitions/NaturalReplication", - "ControlledReplication": "#/definitions/ControlledReplication", - "StaticConcept": "#/definitions/StaticConcept", - "ReversibleFlux": "#/definitions/ReversibleFlux" - } + "ControlledConversion": "#/$defs/ControlledConversion", + "ControlledDegradation": "#/$defs/ControlledDegradation", + "ControlledProduction": "#/$defs/ControlledProduction", + "ControlledReplication": "#/$defs/ControlledReplication", + "GroupedControlledConversion": "#/$defs/GroupedControlledConversion", + "GroupedControlledDegradation": "#/$defs/GroupedControlledDegradation", + "GroupedControlledProduction": "#/$defs/GroupedControlledProduction", + "MultiConversion": "#/$defs/MultiConversion", + "NaturalConversion": "#/$defs/NaturalConversion", + "NaturalDegradation": "#/$defs/NaturalDegradation", + "NaturalProduction": "#/$defs/NaturalProduction", + "NaturalReplication": "#/$defs/NaturalReplication", + "ReversibleFlux": "#/$defs/ReversibleFlux", + "StaticConcept": "#/$defs/StaticConcept" + }, + "propertyName": "type" }, "oneOf": [ { - "$ref": "#/definitions/NaturalConversion" + "$ref": "#/$defs/NaturalConversion" }, { - "$ref": "#/definitions/MultiConversion" + "$ref": "#/$defs/MultiConversion" }, { - "$ref": "#/definitions/ControlledConversion" + "$ref": "#/$defs/ControlledConversion" }, { - "$ref": "#/definitions/NaturalDegradation" + "$ref": "#/$defs/NaturalDegradation" }, { - "$ref": "#/definitions/ControlledDegradation" + "$ref": "#/$defs/ControlledDegradation" }, { - "$ref": "#/definitions/GroupedControlledDegradation" + "$ref": "#/$defs/GroupedControlledDegradation" }, { - "$ref": "#/definitions/NaturalProduction" + "$ref": "#/$defs/NaturalProduction" }, { - "$ref": "#/definitions/ControlledProduction" + "$ref": "#/$defs/ControlledProduction" }, { - "$ref": "#/definitions/GroupedControlledConversion" + "$ref": "#/$defs/GroupedControlledConversion" }, { - "$ref": "#/definitions/GroupedControlledProduction" + "$ref": "#/$defs/GroupedControlledProduction" }, { - "$ref": "#/definitions/NaturalReplication" + "$ref": "#/$defs/NaturalReplication" }, { - "$ref": "#/definitions/ControlledReplication" + "$ref": "#/$defs/ControlledReplication" }, { - "$ref": "#/definitions/StaticConcept" + "$ref": "#/$defs/StaticConcept" }, { - "$ref": "#/definitions/ReversibleFlux" + "$ref": "#/$defs/ReversibleFlux" } ] - } + }, + "title": "Templates", + "type": "array" }, "parameters": { - "title": "Parameters", - "description": "A dict of parameter values where keys correspond to how the parameter appears in rate laws.", - "type": "object", "additionalProperties": { - "$ref": "#/definitions/Parameter" - } + "$ref": "#/$defs/Parameter" + }, + "description": "A dict of parameter values where keys correspond to how the parameter appears in rate laws.", + "title": "Parameters", + "type": "object" }, "initials": { - "title": "Initials", - "description": "A dict of initial condition values where keyscorrespond to concept names they apply to.", - "type": "object", "additionalProperties": { - "$ref": "#/definitions/Initial" - } + "$ref": "#/$defs/Initial" + }, + "description": "A dict of initial condition values where keyscorrespond to concept names they apply to.", + "title": "Initials", + "type": "object" }, "observables": { - "title": "Observables", - "description": "A list of observables that are readouts from the model.", - "type": "object", "additionalProperties": { - "$ref": "#/definitions/Observable" - } + "$ref": "#/$defs/Observable" + }, + "description": "A list of observables that are readouts from the model.", + "title": "Observables", + "type": "object" }, "annotations": { - "title": "Annotations", - "description": "A structure containing model-level annotations. Note that all annotations are optional.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/$defs/Annotations" + }, { - "$ref": "#/definitions/Annotations" + "type": "null" } - ] + ], + "default": null, + "description": "A structure containing model-level annotations. Note that all annotations are optional." }, "time": { - "title": "Time", - "description": "A structure containing time-related annotations. Note that all annotations are optional.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/$defs/Time" + }, { - "$ref": "#/definitions/Time" + "type": "null" } - ] + ], + "default": null, + "description": "A structure containing time-related annotations. Note that all annotations are optional." } }, "required": [ "templates" - ] + ], + "title": "TemplateModel", + "type": "object" + }, + "Time": { + "description": "A special type of Concept that represents time.", + "properties": { + "name": { + "default": "t", + "description": "The symbol of the time variable in the model.", + "title": "Name", + "type": "string" + }, + "units": { + "anyOf": [ + { + "$ref": "#/$defs/Unit" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The units of the time variable." + } + }, + "title": "Time", + "type": "object" + }, + "Unit": { + "description": "A unit of measurement.", + "properties": { + "expression": { + "anyOf": [], + "description": "The expression for the unit.", + "format": "sympy-expr", + "title": "Expression", + "type": "string" + } + }, + "required": [ + "expression" + ], + "title": "Unit", + "type": "object" + } + }, + "properties": { + "Concept": { + "$ref": "#/$defs/Concept" + }, + "Template": { + "$ref": "#/$defs/Template" + }, + "ControlledConversion": { + "$ref": "#/$defs/ControlledConversion" + }, + "GroupedControlledConversion": { + "$ref": "#/$defs/GroupedControlledConversion" + }, + "GroupedControlledProduction": { + "$ref": "#/$defs/GroupedControlledProduction" + }, + "ControlledProduction": { + "$ref": "#/$defs/ControlledProduction" + }, + "NaturalConversion": { + "$ref": "#/$defs/NaturalConversion" + }, + "MultiConversion": { + "$ref": "#/$defs/MultiConversion" + }, + "ReversibleFlux": { + "$ref": "#/$defs/ReversibleFlux" + }, + "NaturalProduction": { + "$ref": "#/$defs/NaturalProduction" + }, + "NaturalDegradation": { + "$ref": "#/$defs/NaturalDegradation" + }, + "ControlledDegradation": { + "$ref": "#/$defs/ControlledDegradation" + }, + "GroupedControlledDegradation": { + "$ref": "#/$defs/GroupedControlledDegradation" + }, + "NaturalReplication": { + "$ref": "#/$defs/NaturalReplication" + }, + "ControlledReplication": { + "$ref": "#/$defs/ControlledReplication" + }, + "StaticConcept": { + "$ref": "#/$defs/StaticConcept" + }, + "TemplateModel": { + "$ref": "#/$defs/TemplateModel" } - } + }, + "required": [ + "Concept", + "Template", + "ControlledConversion", + "GroupedControlledConversion", + "GroupedControlledProduction", + "ControlledProduction", + "NaturalConversion", + "MultiConversion", + "ReversibleFlux", + "NaturalProduction", + "NaturalDegradation", + "ControlledDegradation", + "GroupedControlledDegradation", + "NaturalReplication", + "ControlledReplication", + "StaticConcept", + "TemplateModel" + ], + "type": "object" } \ No newline at end of file From 9ccfcbad55f9df29aba71d551889f16e131bd32d Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Wed, 11 Sep 2024 09:53:03 -0400 Subject: [PATCH 19/32] Initial update of deprecated Pydantic1 code, fix test after parameter update --- mira/dkg/api.py | 18 ++++---- mira/dkg/askemo/api.py | 9 ++-- mira/dkg/client.py | 2 +- mira/dkg/construct_registry.py | 3 +- mira/dkg/model.py | 10 ++--- mira/dkg/web_client.py | 3 +- mira/metamodel/io.py | 2 +- mira/metamodel/schema.py | 2 +- mira/metamodel/templates.py | 4 +- mira/modeling/acsets/petri.py | 4 +- mira/modeling/amr/petrinet.py | 16 +++---- mira/modeling/amr/regnet.py | 12 +++--- mira/sources/acsets/stockflow.py | 6 +-- mira/sources/amr/petrinet.py | 11 ++--- mira/sources/amr/stockflow.py | 11 ++--- mira/sources/biomodels.py | 2 +- tests/test_amr_source.py | 6 +-- tests/test_metamodel.py | 4 +- tests/test_model_api.py | 67 ++++++++++++++++------------- tests/test_modeling/test_amr_ops.py | 2 +- tests/test_modeling/test_petri.py | 2 +- tests/test_template.py | 2 +- 22 files changed, 104 insertions(+), 94 deletions(-) diff --git a/mira/dkg/api.py b/mira/dkg/api.py index 34dc33289..dffbdc31b 100644 --- a/mira/dkg/api.py +++ b/mira/dkg/api.py @@ -203,7 +203,7 @@ def get_relations( request: Request, relation_query: RelationQuery = Body( ..., - examples={ + examples=[{ "source type query": { "summary": "Query relations with a given source node type", "value": { @@ -285,7 +285,7 @@ def get_relations( "full": True, }, }, - }, + }], ), ): """Get relations based on the query sent. @@ -392,10 +392,10 @@ def add_resources( request: Request, resource_prefix_list: List[str] = Body( ..., - description="A of resources to add to the DKG", + description="A list of resources to add to the DKG", title="Resource Prefixes", - example=["probonto", "wikidata", "eiffel", "geonames", "ncit", - "nbcbitaxon"], + examples=[["probonto", "wikidata", "eiffel", "geonames", "ncit", + "nbcbitaxon"]], ) ): """From a list of resource prefixes, add a list of nodes and edges @@ -446,7 +446,7 @@ def is_ontological_child( request: Request, query: IsOntChildQuery = Body( ..., - example={"child_curie": "vo:0001113", "parent_curie": "obi:0000047"}, + examples=[{"child_curie": "vo:0001113", "parent_curie": "obi:0000047"}], ) ): """Check if one CURIE is an ontological child of another CURIE""" @@ -490,7 +490,7 @@ def search( labels: Optional[str] = Query( default=None, description="A comma-separated list of labels", - examples={ + examples=[{ "no label filter": { "summary": "Don't filter by label", "value": None, @@ -499,7 +499,7 @@ def search( "summary": "Search for units, which are labeled as `unit`", "value": "unit", }, - }, + }], ), wikidata_fallback: bool = Query( default=False, @@ -530,7 +530,7 @@ class ParentQuery(BaseModel): def common_parent( request: Request, query: ParentQuery = Body( - ..., example={"curie1": "ido:0000566", "curie2": "ido:0000567"} + ..., examples=[{"curie1": "ido:0000566", "curie2": "ido:0000567"}] ), ): """Get the common parent of two CURIEs""" diff --git a/mira/dkg/askemo/api.py b/mira/dkg/askemo/api.py index ade09fdad..5a9f2b67a 100644 --- a/mira/dkg/askemo/api.py +++ b/mira/dkg/askemo/api.py @@ -70,7 +70,7 @@ def get_askemo_terms() -> Mapping[str, Term]: """Load the epi ontology JSON.""" rv = {} for obj in json.loads(ONTOLOGY_PATH.read_text()): - term = Term.parse_obj(obj) + term = Term.model_validate(obj) rv[term.id] = term return rv @@ -79,7 +79,7 @@ def get_askemosw_terms() -> Mapping[str, Term]: """Load the space weather ontology JSON.""" rv = {} for obj in json.loads(SW_ONTOLOGY_PATH.read_text()): - term = Term.parse_obj(obj) + term = Term.model_validate(obj) rv[term.id] = term return rv @@ -87,14 +87,15 @@ def get_askem_climate_ontology_terms() -> Mapping[str, Term]: """Load the space weather ontology JSON.""" rv = {} for obj in json.loads(CLIMATE_ONTOLOGY_PATH.read_text()): - term = Term.parse_obj(obj) + term = Term.model_validate(obj) rv[term.id] = term return rv def write(ontology: Mapping[str, Term], path: Path) -> None: terms = [ - term.dict(exclude_unset=True, exclude_defaults=True, exclude_none=True) + term.model_dump(exclude_unset=True, exclude_defaults=True, + exclude_none=True) for _curie, term in sorted(ontology.items()) ] path.write_text( diff --git a/mira/dkg/client.py b/mira/dkg/client.py index d0f1df77f..972a84c7e 100644 --- a/mira/dkg/client.py +++ b/mira/dkg/client.py @@ -209,7 +209,7 @@ def as_askem_entity(self): raise ValueError(f"can only call as_askem_entity() on ASKEM ontology terms") if isinstance(self, AskemEntity): return self - data = self.dict() + data = self.model_dump() return AskemEntity( **data, physical_min=self._get_single_property( diff --git a/mira/dkg/construct_registry.py b/mira/dkg/construct_registry.py index bde7c12bd..a0a8d1743 100644 --- a/mira/dkg/construct_registry.py +++ b/mira/dkg/construct_registry.py @@ -140,7 +140,8 @@ def _construct_registry( ) output_path.write_text( - json.dumps(new_config.dict(exclude_none=True, exclude_unset=True), indent=2) + json.dumps(new_config.model_dump(exclude_none=True, + exclude_unset=True), indent=2) ) if upload: upload_s3(output_path, use_case="epi") diff --git a/mira/dkg/model.py b/mira/dkg/model.py index 5d964a569..77d995d0e 100644 --- a/mira/dkg/model.py +++ b/mira/dkg/model.py @@ -406,12 +406,12 @@ def biomodels_id_to_model( model_id: str = FastPath( ..., description="The BioModels model ID to get the template model for.", - example="BIOMD0000000956", + examples=["BIOMD0000000956"], ), simplify: bool = Query( default=True, description="Whether to simplify the rate laws of the model.", - example=True, + examples=[True], ), aggregate_params: bool = Query( default=False, @@ -419,7 +419,7 @@ def biomodels_id_to_model( "a new parameter to make rate laws more mass action like" "if the actual rate law uses some function of constants" "and one or more parameters.", - example=False, + examples=[False], ) ): """Get a BioModels base template model by providing its model id""" @@ -692,7 +692,7 @@ def model_comparison( template_models, refinement_func=request.app.state.refinement_closure.is_ontological_child ) resp = ModelComparisonResponse( - graph_comparison_data=graph_comparison_data.dict(), + graph_comparison_data=graph_comparison_data.model_dump(), similarity_scores=graph_comparison_data.get_similarity_scores(), ) return resp @@ -725,7 +725,7 @@ def askepetrinet_model_comparison( app.state.refinement_closure.is_ontological_child ) resp = ModelComparisonResponse( - graph_comparison_data=graph_comparison_data.dict(), + graph_comparison_data=graph_comparison_data.model_dump(), similarity_scores=graph_comparison_data.get_similarity_scores(), ) return resp diff --git a/mira/dkg/web_client.py b/mira/dkg/web_client.py index 677630f25..1529578ac 100644 --- a/mira/dkg/web_client.py +++ b/mira/dkg/web_client.py @@ -130,7 +130,8 @@ def get_relations_web( print(relations[:5]) """ - query_json = relations_model.dict(exclude_unset=True, exclude_defaults=True) + query_json = relations_model.model_dump(exclude_unset=True, + exclude_defaults=True) res_json = web_client( endpoint="/relations", method="post", query_json=query_json, api_url=api_url ) diff --git a/mira/metamodel/io.py b/mira/metamodel/io.py index 09bdaa2ef..6c30fed44 100644 --- a/mira/metamodel/io.py +++ b/mira/metamodel/io.py @@ -35,7 +35,7 @@ def model_to_json_file(model: TemplateModel, fname): A file path to dump the model into. """ with open(fname, 'w') as fh: - json.dump(json.loads(model.json()), fh, indent=1) + json.dump(json.loads(model.model_dump_json()), fh, indent=1) def expression_to_mathml(expression: sympy.Expr, *args, **kwargs) -> str: diff --git a/mira/metamodel/schema.py b/mira/metamodel/schema.py index a7a321738..c2a994630 100644 --- a/mira/metamodel/schema.py +++ b/mira/metamodel/schema.py @@ -46,7 +46,7 @@ def get_json_schema(): def _encoder(x): if isinstance(x, BaseModel): - return x.dict() + return x.model_dump() return x diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index f11c12799..f0d292d48 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -779,7 +779,7 @@ def with_mass_action_rate_law(self, parameter, independent=False) -> "Template": : A copy of this template with the mass action rate law. """ - template = self.copy(deep=True) + template = self.model_copy(deep=True) template.set_mass_action_rate_law(parameter, independent=independent) return template @@ -803,7 +803,7 @@ def set_rate_law(self, rate_law: Union[str, sympy.Expr, SympyExprStr], def with_rate_law(self, rate_law: Union[str, sympy.Expr, SympyExprStr], local_dict=None) -> "Template": - template = self.copy(deep=True) + template = self.model_copy(deep=True) template.set_rate_law(rate_law, local_dict=local_dict) return template diff --git a/mira/modeling/acsets/petri.py b/mira/modeling/acsets/petri.py index b18af6b5a..e23d70f95 100644 --- a/mira/modeling/acsets/petri.py +++ b/mira/modeling/acsets/petri.py @@ -94,7 +94,7 @@ def __init__(self, model: Model): 'is_observable': False, 'mira_ids': ids, 'mira_context': context, - 'mira_concept': var.concept.json(), + 'mira_concept': var.concept.model_dump_json(), } } initial_expr = var.data.get('expression') @@ -125,7 +125,7 @@ def __init__(self, model: Model): 'parameter_name': pname, 'parameter_value': pvalue, 'parameter_distribution': distr, - 'mira_template': transition.template.json(), + 'mira_template': transition.template.model_dump_json(), } } transition_dict["rate"] = pvalue diff --git a/mira/modeling/amr/petrinet.py b/mira/modeling/amr/petrinet.py index c3b491301..cb421097f 100644 --- a/mira/modeling/amr/petrinet.py +++ b/mira/modeling/amr/petrinet.py @@ -294,15 +294,15 @@ def to_pydantic(self, name=None, description=None, model_version=None) -> "Model ), properties=self.properties, model=PetriModel( - states=[State.parse_obj(s) for s in self.states], - transitions=[Transition.parse_obj(t) for t in self.transitions], + states=[State.model_validate(s) for s in self.states], + transitions=[Transition.model_validate(t) for t in self.transitions], ), semantics=Ode(ode=OdeSemantics( - rates=[Rate.parse_obj(r) for r in self.rates], - initials=[Initial.parse_obj(i) for i in self.initials], - parameters=[Parameter.parse_obj(p) for p in self.parameters], - observables=[Observable.parse_obj(o) for o in self.observables], - time=Time.parse_obj(self.time) if self.time else Time(id='t') + rates=[Rate.model_validate(r) for r in self.rates], + initials=[Initial.model_validate(i) for i in self.initials], + parameters=[Parameter.model_validate(p) for p in self.parameters], + observables=[Observable.model_validate(o) for o in self.observables], + time=Time.model_validate(self.time) if self.time else Time(id='t') )), metadata=self.metadata, ) @@ -428,7 +428,7 @@ class Parameter(BaseModel): def from_dict(cls, d): d = deepcopy(d) d['id'] = str(d['id']) - return cls.parse_obj(d) + return cls.model_validate(d) class Time(BaseModel): diff --git a/mira/modeling/amr/regnet.py b/mira/modeling/amr/regnet.py index 62b3dec14..cdbdbe0ba 100644 --- a/mira/modeling/amr/regnet.py +++ b/mira/modeling/amr/regnet.py @@ -377,15 +377,15 @@ def to_pydantic( model_version=model_version or '0.1', ), model=RegNetModel( - vertices=[State.parse_obj(s) for s in self.states], - edges=[Transition.parse_obj(t) for t in self.transitions], + vertices=[State.model_validate(s) for s in self.states], + edges=[Transition.model_validate(t) for t in self.transitions], parameters=[Parameter.from_dict(p) for p in self.parameters], ), semantics=Ode( ode=OdeSemantics( - rates=[Rate.parse_obj(r) for r in self.rates], - observables=[Observable.parse_obj(o) for o in self.observables], - time=Time.parse_obj(self.time) if self.time else Time(id='t') + rates=[Rate.model_validate(r) for r in self.rates], + observables=[Observable.model_validate(o) for o in self.observables], + time=Time.model_validate(self.time) if self.time else Time(id='t') ) ), metadata=self.metadata, @@ -498,7 +498,7 @@ class Parameter(BaseModel): def from_dict(cls, d): d = deepcopy(d) d['id'] = str(d['id']) - return cls.parse_obj(d) + return cls.model_validate(d) class RegNetModel(BaseModel): diff --git a/mira/sources/acsets/stockflow.py b/mira/sources/acsets/stockflow.py index f274ed226..e573060ae 100644 --- a/mira/sources/acsets/stockflow.py +++ b/mira/sources/acsets/stockflow.py @@ -124,9 +124,9 @@ def template_model_from_stockflow_ascet_json(model_json) -> TemplateModel: if (link["t"] == flow_id and link["s"] != input) ] - input_concepts = [concepts[i].copy(deep=True) for i in inputs] - output_concepts = [concepts[i].copy(deep=True) for i in outputs] - controller_concepts = [concepts[i].copy(deep=True) for i in controllers] + input_concepts = [concepts[i].model_copy(deep=True) for i in inputs] + output_concepts = [concepts[i].model_copy(deep=True) for i in outputs] + controller_concepts = [concepts[i].model_copy(deep=True) for i in controllers] expression_sympy = safe_parse_expr(expression_str, symbols) diff --git a/mira/sources/amr/petrinet.py b/mira/sources/amr/petrinet.py index 2c4c3d244..8e9c670f2 100644 --- a/mira/sources/amr/petrinet.py +++ b/mira/sources/amr/petrinet.py @@ -128,7 +128,7 @@ def template_model_from_amr_json(model_json) -> TemplateModel: continue try: initial = Initial( - concept=concepts[initial_state['target']].copy(deep=True), + concept=concepts[initial_state['target']].model_copy(deep=True), expression=initial_expr ) initials[initial.concept.name] = initial @@ -197,9 +197,10 @@ def template_model_from_amr_json(model_json) -> TemplateModel: both = set(inputs) & set(outputs) # We can now get the appropriate concepts for each group - input_concepts = [concepts[i].copy(deep=True) for i in inputs] - output_concepts = [concepts[i].copy(deep=True) for i in outputs] - controller_concepts = [concepts[i].copy(deep=True) for i in controllers] + input_concepts = [concepts[i].model_copy(deep=True) for i in inputs] + output_concepts = [concepts[i].model_copy(deep=True) for i in outputs] + controller_concepts = [concepts[i].model_copy(deep=True) for i in + controllers] transition_id = transition['id'] rate_law = get_sympy(rate_obj, local_dict=symbols) @@ -211,7 +212,7 @@ def template_model_from_amr_json(model_json) -> TemplateModel: # Handle static states static_states = all_states - used_states for state in static_states: - concept = concepts[state].copy(deep=True) + concept = concepts[state].model_copy(deep=True) templates.append(StaticConcept(subject=concept)) # Finally, we gather some model-level annotations diff --git a/mira/sources/amr/stockflow.py b/mira/sources/amr/stockflow.py index c3ca7cb4b..fecac706f 100644 --- a/mira/sources/amr/stockflow.py +++ b/mira/sources/amr/stockflow.py @@ -67,7 +67,7 @@ def template_model_from_amr_json(model_json) -> TemplateModel: continue try: initial = Initial( - concept=concepts[initial_state['target']].copy(deep=True), + concept=concepts[initial_state['target']].model_copy(deep=True), expression=initial_expr ) initials[initial.concept.name] = initial @@ -114,9 +114,10 @@ def template_model_from_amr_json(model_json) -> TemplateModel: and link['source'] in concepts and link['source'] not in aux_expressions)] - input_concepts = [concepts[input].copy(deep=True)] if input else [] - output_concepts = [concepts[output].copy(deep=True)] if output else [] - controller_concepts = [concepts[i].copy(deep=True) for i in controllers] + input_concepts = [concepts[input].model_copy(deep=True)] if input \ + else [] + output_concepts = [concepts[output].model_copy(deep=True)] if output else [] + controller_concepts = [concepts[i].model_copy(deep=True) for i in controllers] if 'rate_expression' in flow: rate_expr = safe_parse_expr(flow['rate_expression'], @@ -133,7 +134,7 @@ def template_model_from_amr_json(model_json) -> TemplateModel: static_stocks = all_stocks - used_stocks for state in static_stocks: - concept = concepts[state].copy(deep=True) + concept = concepts[state].model_copy(deep=True) templates.append(StaticConcept(subject=concept)) # Finally, we gather some model-level annotations diff --git a/mira/sources/biomodels.py b/mira/sources/biomodels.py index 19f7cea81..67a9bd148 100644 --- a/mira/sources/biomodels.py +++ b/mira/sources/biomodels.py @@ -211,7 +211,7 @@ def main(): tqdm.write(f"[{model_id}] failed to parse: {e}") continue model_module.join(name=f"{model_id}.json").write_text( - template_model.json(indent=2) + template_model.model_dump_json(indent=2) ) # Write a petri-net type graphical representation of the model diff --git a/tests/test_amr_source.py b/tests/test_amr_source.py index 0b290f5a7..fdfce2d55 100644 --- a/tests/test_amr_source.py +++ b/tests/test_amr_source.py @@ -196,9 +196,9 @@ def test_annotation_serialization_ingestion(): petrinet_tm = petrinet.template_model_from_amr_json(amrs[1]) stockflow_tm = stockflow.template_model_from_amr_json(amrs[2]) - zipped_annotations = zip(regnet_tm.annotations.dict().values(), - petrinet_tm.annotations.dict().values(), - stockflow_tm.annotations.dict().values()) + zipped_annotations = zip(regnet_tm.annotations.model_dump().values(), + petrinet_tm.annotations.model_dump().values(), + stockflow_tm.annotations.model_dump().values()) for annotation_attribute_tuple in zipped_annotations: assert annotation_attribute_tuple[0] diff --git a/tests/test_metamodel.py b/tests/test_metamodel.py index accdf19be..5ca5923fc 100644 --- a/tests/test_metamodel.py +++ b/tests/test_metamodel.py @@ -235,8 +235,8 @@ def test_from_askenet_petri_mathml(): tm = template_model_from_amr_json(model_json) # Check equality - mathml_str = sorted_json_str(mathml_tm.dict()) - org_str = sorted_json_str(tm.dict()) + mathml_str = sorted_json_str(mathml_tm.model_dump()) + org_str = sorted_json_str(tm.model_dump()) assert mathml_str == org_str diff --git a/tests/test_model_api.py b/tests/test_model_api.py index b92fe907a..de3e97ef8 100644 --- a/tests/test_model_api.py +++ b/tests/test_model_api.py @@ -96,13 +96,13 @@ def query_relations( relations=relation_type, ) res = get_relations_web(relations_model=rq) - return [r.dict(exclude_unset=True) for r in res] + return [r.model_dump(exclude_unset=True) for r in res] @staticmethod def get_entity(curie: str): try: res = get_entity_web(curie=curie) - return res.dict(exclude_unset=True) + return res.model_dump(exclude_unset=True) except requests.exceptions.HTTPError: return None @@ -143,10 +143,10 @@ def test_petri(self): """Test the petrinet endpoint.""" sir_model_templ = _get_sir_templatemodel() response = self.client.post( - "/api/to_petrinet_acsets", json=sir_model_templ.dict() + "/api/to_petrinet_acsets", json=sir_model_templ.model_dump() ) self.assertEqual(response.status_code, 200, msg=response.content) - response_petri_net = PetriNetResponse.parse_obj(response.json()) + response_petri_net = PetriNetResponse.model_validate(response.json()) model = Model(sir_model_templ) petri_net = PetriNetModel(model) self.assertEqual(petri_net.to_pydantic(), response_petri_net) @@ -166,9 +166,10 @@ def test_petri_distribution(self): "/api/to_petrinet_acsets", json=json.loads(sir_distribution.json()) ) pm = response.json() - assert pm['T'][0]['tprop']['parameter_distribution'] == distr.json() + assert (pm['T'][0]['tprop']['parameter_distribution'] == + distr.model_dump_json()) assert json.loads(pm['T'][0]['tprop']['mira_parameter_distributions']) == \ - {'beta': distr.dict()} + {'beta': distr.model_dump()} self.assertEqual(200, response.status_code, msg=response.content) def test_petri_to_template_model(self): @@ -177,7 +178,7 @@ def test_petri_to_template_model(self): response = self.client.post("/api/from_petrinet_acsets", json=petrinet_json) self.assertEqual(200, response.status_code, msg=response.content) resp_json_str = sorted_json_str(response.json()) - tm_json_str = sorted_json_str(tm.dict()) + tm_json_str = sorted_json_str(tm.model_dump()) self.assertEqual(resp_json_str, tm_json_str) def test_petri_to_template_model_parameterized(self): @@ -186,7 +187,7 @@ def test_petri_to_template_model_parameterized(self): response = self.client.post("/api/from_petrinet_acsets", json=petrinet_json) self.assertEqual(200, response.status_code, msg=response.content) resp_json_str = sorted_json_str(response.json()) - tm_json_str = sorted_json_str(tm.dict()) + tm_json_str = sorted_json_str(tm.model_dump()) self.assertEqual(resp_json_str, tm_json_str) def test_askenet_to_template_model(self): @@ -223,7 +224,7 @@ def test_stratify(self): "geonames:4930956": "Boston", } query_json = { - "template_model": sir_templ_model.dict(), + "template_model": sir_templ_model.model_dump(), "key": key, "strata": strata, "strata_name_map": strata_name_map, @@ -238,13 +239,13 @@ def test_stratify(self): strata=strata, strata_curie_to_name=strata_name_map, ) - strat_str = sorted_json_str(strat_templ_model.dict()) + strat_str = sorted_json_str(strat_templ_model.model_dump()) self.assertEqual(strat_str, resp_json_str) # Test directed True, also skip the name map query_json = { - "template_model": sir_templ_model.dict(), + "template_model": sir_templ_model.model_dump(), "key": key, "strata": strata, "directed": True, @@ -259,7 +260,7 @@ def test_stratify(self): strata=set(strata), directed=query_json["directed"], ) - strat_str = sorted_json_str(strat_templ_model.dict()) + strat_str = sorted_json_str(strat_templ_model.model_dump()) self.assertEqual(strat_str, resp_json_str) @@ -291,7 +292,7 @@ def test_stratify_observable_api(self): def test_to_dot_file(self): sir_templ_model = _get_sir_templatemodel() response = self.client.post( - "/api/viz/to_dot_file", json=sir_templ_model.dict() + "/api/viz/to_dot_file", json=sir_templ_model.model_dump() ) self.assertEqual(200, response.status_code) self.assertIn( @@ -309,7 +310,7 @@ def test_to_dot_file(self): def test_to_graph_image(self): sir_templ_model = _get_sir_templatemodel() response = self.client.post( - "/api/viz/to_image", json=sir_templ_model.dict() + "/api/viz/to_image", json=sir_templ_model.model_dump() ) self.assertEqual(200, response.status_code) self.assertIn( @@ -343,7 +344,8 @@ def test_biomodels_id_to_template_model(self): assert tm == local self.assertEqual( - sorted_json_str(tm.dict()), sorted_json_str(local.dict()) + sorted_json_str(tm.model_dump()), sorted_json_str( + local.model_dump()) ) def test_workflow(self): @@ -379,7 +381,7 @@ def test_template_model_to_bilayer_json(self): bj = BilayerModel(Model(tm)).bilayer response = self.client.post("/api/model_to_bilayer", - json=json.loads(tm.json())) + json=json.loads(tm.model_dump_json())) self.assertEqual(response.status_code, 200) bj_res = response.json() @@ -400,7 +402,8 @@ def test_xml_str_to_template_model(self): # less restrictive than the string comparison below assert tm_res == local self.assertEqual( - sorted_json_str(tm_res.dict()), sorted_json_str(local.dict()) + sorted_json_str(tm_res.model_dump()), sorted_json_str( + local.model_dump()) ) def test_models_to_templatemodel_delta_graph_json(self): @@ -415,8 +418,8 @@ def test_models_to_templatemodel_delta_graph_json(self): response = self.client.post( "/api/models_to_delta_graph", json={ - "template_model1": sir_templ_model.dict(), - "template_model2": sir_templ_model_ctx.dict(), + "template_model1": sir_templ_model.model_dump(), + "template_model2": sir_templ_model_ctx.model_dump(), }, ) self.assertEqual(200, response.status_code) @@ -445,8 +448,8 @@ def test_models_to_templatemodel_delta_graph_image(self): response = self.client.post( "/api/models_to_delta_image", json={ - "template_model1": sir_templ_model.dict(), - "template_model2": sir_templ_model_ctx.dict(), + "template_model1": sir_templ_model.model_dump(), + "template_model2": sir_templ_model_ctx.model_dump(), }, ) self.assertEqual(200, response.status_code) @@ -477,7 +480,7 @@ def test_add_transition(self): response = self.client.post( "/api/add_transition", json={ - "template_model": sir_templ_model.dict(), + "template_model": sir_templ_model.model_dump(), "subject_concept": s, "outcome_concept": x, "parameter": {'name': 's_to_x', 'value': 0.1}} @@ -496,7 +499,7 @@ def test_n_way_comparison(self): response = self.client.post( "/api/model_comparison", - json={"template_models": [m.dict() for m in mmts]}, + json={"template_models": [m.model_dump() for m in mmts]}, ) self.assertEqual(200, response.status_code) @@ -514,8 +517,8 @@ def test_n_way_comparison(self): similarity_scores=model_comparson_graph_data.get_similarity_scores(), ) self.assertEqual( - sorted_json_str(local_response.dict()), - sorted_json_str(resp_model.dict()), + sorted_json_str(local_response.model_dump()), + sorted_json_str(resp_model.model_dump()), ) def test_n_way_comparison_askenet(self): @@ -531,17 +534,18 @@ def test_n_way_comparison_askenet(self): # Copy parameters, annotations, initials and observables from the # original model sir_parameterized_ctx.parameters = { - k: v.copy(deep=True) + k: v.model_copy(deep=True) for k, v in sir_templ_model.parameters.items() } sir_parameterized_ctx.annotations = \ - sir_templ_model.annotations.copy(deep=True) + sir_templ_model.annotations.model_copy(deep=True) sir_parameterized_ctx.observables = { - k: v.copy(deep=True) + k: v.model_copy(deep=True) for k, v in sir_templ_model.observables.items() } sir_parameterized_ctx.initials = { - k: v.copy(deep=True) for k, v in sir_templ_model.initials.items() + k: v.model_copy(deep=True) for k, v in + sir_templ_model.initials.items() } sir_parameterized_ctx.time = copy.deepcopy(sir_templ_model.time) askenet_list = [] @@ -587,11 +591,12 @@ def test_n_way_comparison_askenet(self): # Compare the ModelComparisonResponse models assert local_response == resp_model # If assertion fails the diff is printed local_sorted_str = sorted_json_str( - json.loads(local_response.json(**dict_options)), + json.loads(local_response.model_dump_json(**dict_options)), skip_empty=True ) resp_sorted_str = sorted_json_str( - json.loads(resp_model.json(**dict_options)), skip_empty=True + json.loads(resp_model.model_dump_json(**dict_options)), + skip_empty=True ) self.assertEqual(local_sorted_str, resp_sorted_str) diff --git a/tests/test_modeling/test_amr_ops.py b/tests/test_modeling/test_amr_ops.py index a3d525e94..e194ed278 100644 --- a/tests/test_modeling/test_amr_ops.py +++ b/tests/test_modeling/test_amr_ops.py @@ -297,7 +297,7 @@ def test_add_parameter(self): description = 'TEST_DESCRIPTION' value = 0.35 distribution = {'type': 'test_distribution', - 'parameters': {'test_dist': 5}} + 'parameters': {'test_dist': 5.0}} new_amr = add_parameter(amr, parameter_id=parameter_id, name=name, description=description, value=value, distribution=distribution) param_dict = {} diff --git a/tests/test_modeling/test_petri.py b/tests/test_modeling/test_petri.py index 7f43d427a..002fb0bbd 100644 --- a/tests/test_modeling/test_petri.py +++ b/tests/test_modeling/test_petri.py @@ -45,4 +45,4 @@ def test_petri_parameterized(): assert js assert js['S'][0]['concentration'] == 1 assert js['T'][0]['rate'] == 0.1 - assert js['T'][0]['tprop']['parameter_distribution'] == distr.json() + assert js['T'][0]['tprop']['parameter_distribution'] == distr.model_dump_json() diff --git a/tests/test_template.py b/tests/test_template.py index aafe7fda3..975477018 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -295,7 +295,7 @@ def test_get_curie_custom(): def test_rate_json(): t = NaturalDegradation(subject=Concept(name='x'), rate_law=sympy.Mul(2, sympy.Symbol('x'))) - jj = json.loads(t.json()) + jj = json.loads(t.model_dump_json()) assert jj.get('rate_law') == '2*x', jj t2 = Template.from_json(jj) assert isinstance(t2, NaturalDegradation) From ffb1c6527f214a0f2edb4834fc199f8a0aded42a Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Wed, 11 Sep 2024 10:23:47 -0400 Subject: [PATCH 20/32] More Pydantic2 deprecation updates, pin pydantic version for tox tests --- mira/sources/acsets/stockflow.py | 2 +- mira/sources/system_dynamics/pysd.py | 12 ++++++------ tox.ini | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mira/sources/acsets/stockflow.py b/mira/sources/acsets/stockflow.py index e573060ae..8d2eefad1 100644 --- a/mira/sources/acsets/stockflow.py +++ b/mira/sources/acsets/stockflow.py @@ -144,7 +144,7 @@ def template_model_from_stockflow_ascet_json(model_json) -> TemplateModel: static_stocks = all_stocks - used_stocks for state in static_stocks: - concept = concepts[state].copy(deep=True) + concept = concepts[state].model_copy(deep=True) templates.append(StaticConcept(subject=concept)) return TemplateModel(templates=templates, parameters=mira_parameters) diff --git a/mira/sources/system_dynamics/pysd.py b/mira/sources/system_dynamics/pysd.py index ed9281fa0..9e79f1e27 100644 --- a/mira/sources/system_dynamics/pysd.py +++ b/mira/sources/system_dynamics/pysd.py @@ -164,7 +164,7 @@ def template_model_from_pysd_model( ): if initials_map and (mapped_value := initials_map.get(state_id)): initial = Initial( - concept=concepts[state_id].copy(deep=True), + concept=concepts[state_id].model_copy(deep=True), expression=SympyExprStr(sympy.Float(mapped_value)), ) # if the state value is not a number @@ -175,12 +175,12 @@ def template_model_from_pysd_model( f"got non-numeric state value for {state_id}: {state_initial_value}" ) initial = Initial( - concept=concepts[state_id].copy(deep=True), + concept=concepts[state_id].model_copy(deep=True), expression=SympyExprStr(sympy.Float("0")), ) else: initial = Initial( - concept=concepts[state_id].copy(deep=True), + concept=concepts[state_id].model_copy(deep=True), expression=SympyExprStr(sympy.Float(state_initial_value)), ) mira_initials[initial.concept.name] = initial @@ -322,15 +322,15 @@ def template_model_from_pysd_model( templates_.extend( transition_to_templates( input_concepts=[ - concepts[input_name].copy(deep=True) + concepts[input_name].model_copy(deep=True) for input_name in transition.get("inputs") ], output_concepts=[ - concepts[output_name].copy(deep=True) + concepts[output_name].model_copy(deep=True) for output_name in transition.get("outputs") ], controller_concepts=[ - concepts[controller_name].copy(deep=True) + concepts[controller_name].model_copy(deep=True) for controller_name in transition.get("controllers") ], transition_rate=( diff --git a/tox.ini b/tox.ini index cd9e4e850..45c16d32e 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,7 @@ commands = [testenv:mypy] deps = mypy - pydantic + pydantic>=2.0.0 skip_install = true commands = mypy --install-types --non-interactive --ignore-missing-imports mira/ description = Run the mypy tool to check static typing on the project. From 1d577ea19a4c411b742a4bd1e09290e396515f7f Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Wed, 11 Sep 2024 13:16:59 -0400 Subject: [PATCH 21/32] Further pydantic2 deprecation updates, remove explicit pydantic install in tox.ini --- mira/dkg/construct.py | 7 +++++- mira/dkg/construct_registry.py | 8 ++++++- mira/dkg/model.py | 39 ++++++++++++++++++++-------------- mira/modeling/acsets/petri.py | 3 +-- tests/test_model_api.py | 2 +- tox.ini | 1 - 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/mira/dkg/construct.py b/mira/dkg/construct.py index 322f6c534..3b8b04ae6 100644 --- a/mira/dkg/construct.py +++ b/mira/dkg/construct.py @@ -563,7 +563,12 @@ def main( ): """Generate the node and edge files.""" if Path(use_case).is_file(): - config = DKGConfig.parse_file(use_case) + with open(use_case, 'r') as file: + file_content = file.read() + if use_case.lower().endswith(".json"): + config = DKGConfig.model_validate_json(file_content) + else: + config = DKGConfig.model_validate(file_content) use_case = config.use_case else: config = None diff --git a/mira/dkg/construct_registry.py b/mira/dkg/construct_registry.py index a0a8d1743..77a3d7605 100644 --- a/mira/dkg/construct_registry.py +++ b/mira/dkg/construct_registry.py @@ -120,7 +120,13 @@ def _construct_registry( edges_path: Optional[Path] = None, upload: bool = False, ): - config = Config.parse_file(config_path) + with config_path.open("r") as file: + file_content = file.read() + + if config_path.suffix.lower() == ".json": + config = Config.model_validate_json(file_content) + else: + config = Config.model_validate(file_content) prefixes = get_prefixes(nodes_path=nodes_path, edges_path=edges_path) manager = Manager( diff --git a/mira/dkg/model.py b/mira/dkg/model.py index 77d995d0e..5af4eb6ba 100644 --- a/mira/dkg/model.py +++ b/mira/dkg/model.py @@ -110,7 +110,9 @@ standard now uses that endpoint. """.rstrip()), ) -def model_to_petri(template_model: Dict[str, Any] = Body(..., example=template_model_example)): +def model_to_petri(template_model: Dict[str, Any] = Body(..., + examples=[ + template_model_example])): """Create a PetriNet model from a TemplateModel""" tm = TemplateModel.from_json(template_model) model = Model(tm) @@ -134,7 +136,8 @@ def model_to_petri(template_model: Dict[str, Any] = Body(..., example=template_m standard now uses that endpoint. """.rstrip()), ) -def petri_to_model(petri_json: Dict[str, Any] = Body(..., example=petrinet_json)): +def petri_to_model(petri_json: Dict[str, Any] = Body(..., + examples=[petrinet_json])): """Create a TemplateModel from a PetriNet model""" return template_model_from_petri_json(petri_json) @@ -151,7 +154,8 @@ def petri_to_model(petri_json: Dict[str, Any] = Body(..., example=petrinet_json) implement this standard. """.rstrip()), ) -def model_to_amr(template_model: Dict[str, Any] = Body(..., example=template_model_example)): +def model_to_amr(template_model: Dict[str, Any] = Body(..., + examples=[template_model_example])): """Create an AMR Petri model from a TemplateModel.""" tm = TemplateModel.from_json(template_model) model = Model(tm) @@ -171,7 +175,8 @@ def model_to_amr(template_model: Dict[str, Any] = Body(..., example=template_mod extension, stratification, and comparison. """.rstrip()), ) -def amr_to_model(amr_json: Dict[str, Any] = Body(..., example=amr_petrinet_json)): +def amr_to_model(amr_json: Dict[str, Any] = Body(..., + examples=[amr_petrinet_json])): """Create a TemplateModel from an AMR model.""" return template_model_from_amr_json(amr_json) @@ -292,13 +297,13 @@ def model_stratification( request: Request, stratification_query: StratificationQuery = Body( ..., - example={ + examples=[{ "template_model": template_model_example, "key": "city", "strata": ["geonames:4930956", "geonames:5128581"], "strata_name_lookup": True, "params_to_stratify": ["beta"], - }, + }], ) ): """Stratify a model according to the specified stratification""" @@ -346,11 +351,11 @@ def model_stratification( def dimension_transform( query: Dict[str, Any] = Body( ..., - example={ + examples=[{ "model": sir_parameterized_init, "counts_unit": "person", "norm_factor": 1e5, - }, + }], ) ): """Convert all entity concentrations to dimensionless units""" @@ -371,11 +376,11 @@ def dimension_transform( def dimension_transform( query: Dict[str, Any] = Body( ..., - example={ + examples=[{ "model": amr_petrinet_json_units_values, "counts_units": "persons", "norm_factor": 1e5, - }, + }], ) ): """Convert all entity concentrations to dimensionless units""" @@ -440,7 +445,7 @@ def bilayer_to_template_model( bilayer: Dict[str, Any] = Body( ..., description="The bilayer json to transform to a template model", - example=sir_bilayer, + examples=[sir_bilayer], ) ): """Transform a bilayer json to a template model""" @@ -453,7 +458,7 @@ def template_model_to_bilayer( template_model: Dict[str, Any] = Body( ..., description="A template model to turn into a bilayer json", - example=template_model_example, + examples=[template_model_example], ) ): """Turn template model into a bilayer json""" @@ -518,7 +523,8 @@ def _graph_model( ) def model_to_viz_dot( bg_task: BackgroundTasks, - template_model: Dict[str, Any] = Body(..., example=template_model_example), + template_model: Dict[str, Any] = Body(..., examples=[ + template_model_example]), ): """Create a graphviz dot file from a TemplateModel""" tm = TemplateModel.from_json(template_model) @@ -540,7 +546,8 @@ def model_to_viz_dot( ) def model_to_graph_image( bg_task: BackgroundTasks, - template_model: Dict[str, Any] = Body(..., example=template_model_example), + template_model: Dict[str, Any] = Body(..., examples=[ + template_model_example]), ): """Create a graph image from a TemplateModel""" tm = TemplateModel.from_json(template_model) @@ -640,13 +647,13 @@ class AddTranstitionQuery(BaseModel): def add_transition( add_transition_query: AddTranstitionQuery = Body( ..., - example={ + examples=[{ "template_model": template_model_example, "subject_concept": {"name": "infected population", "identifiers": {"ido": "0000511"}}, "outcome_concept": {"name": "dead", "identifiers": {"ncit": "C28554"}}, - }, + }], ) ): """Add a transition between two concepts in a template model""" diff --git a/mira/modeling/acsets/petri.py b/mira/modeling/acsets/petri.py index e23d70f95..e7de0e839 100644 --- a/mira/modeling/acsets/petri.py +++ b/mira/modeling/acsets/petri.py @@ -114,8 +114,7 @@ def __init__(self, model: Model): pname = f"p_petri_{idx + 1}" else: pname = transition.rate.key - - distr = transition.rate.distribution.json() \ + distr = transition.rate.distribution.model_dump_json() \ if transition.rate.distribution else None pvalue = transition.rate.value transition_dict = { diff --git a/tests/test_model_api.py b/tests/test_model_api.py index de3e97ef8..8c34a995b 100644 --- a/tests/test_model_api.py +++ b/tests/test_model_api.py @@ -269,7 +269,7 @@ def test_stratify(self): def test_stratify_observable_api(self): from mira.examples.sir import sir_parameterized - tm = sir_parameterized.copy(deep=True) + tm = sir_parameterized.model_copy(deep=True) symbols = set(tm.get_concepts_name_map().keys()) expr = sympy.Add(*[sympy.Symbol(s) for s in symbols]) tm.observables = {'half_population': Observable( diff --git a/tox.ini b/tox.ini index 45c16d32e..6dbbbce7c 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,6 @@ commands = [testenv:mypy] deps = mypy - pydantic>=2.0.0 skip_install = true commands = mypy --install-types --non-interactive --ignore-missing-imports mira/ description = Run the mypy tool to check static typing on the project. From ba5dd6b6ebcb68bbb8dae8277204940f5d4e882b Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Wed, 11 Sep 2024 14:27:02 -0400 Subject: [PATCH 22/32] Further manual updates, update protected namespace for Header class --- mira/modeling/acsets/petri.py | 4 ++-- mira/modeling/amr/petrinet.py | 3 ++- mira/modeling/amr/regnet.py | 4 ++-- mira/modeling/amr/utils.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mira/modeling/acsets/petri.py b/mira/modeling/acsets/petri.py index e7de0e839..3baa9cddc 100644 --- a/mira/modeling/acsets/petri.py +++ b/mira/modeling/acsets/petri.py @@ -148,7 +148,7 @@ def __init__(self, model: Model): continue key = p.key if p.key else f"p_petri_{idx + 1}" _parameters[key] = p.value - _distributions[key] = p.distribution.dict() \ + _distributions[key] = p.distribution.model_dump() \ if p.distribution else None transition_dict["tprop"]["mira_parameters"] = \ json.dumps(_parameters, sort_keys=True) @@ -186,7 +186,7 @@ def __init__(self, model: Model): key = sanitize_parameter_name( p.key) if p.key else f"p_petri_{idx + 1}" _parameters[key] = p.value - _distributions[key] = p.distribution.dict() \ + _distributions[key] = p.distribution.model_dump() \ if p.distribution else None obs_dict = { 'concept': json.dumps(concept_data), diff --git a/mira/modeling/amr/petrinet.py b/mira/modeling/amr/petrinet.py index cb421097f..7dc93e7f9 100644 --- a/mira/modeling/amr/petrinet.py +++ b/mira/modeling/amr/petrinet.py @@ -10,7 +10,7 @@ from copy import deepcopy from typing import Dict, List, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from mira.metamodel import expression_to_mathml, TemplateModel, SympyExprStr from mira.sources.amr import sanity_check_amr @@ -462,6 +462,7 @@ class Ode(BaseModel): class Header(BaseModel): + model_config = ConfigDict(protected_namespaces=()) name: str schema_url: str = Field(..., alias='schema') schema_name: str diff --git a/mira/modeling/amr/regnet.py b/mira/modeling/amr/regnet.py index cdbdbe0ba..e35a322a1 100644 --- a/mira/modeling/amr/regnet.py +++ b/mira/modeling/amr/regnet.py @@ -11,8 +11,7 @@ from collections import defaultdict from typing import Dict, List, Optional, Union -import sympy -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from mira.metamodel import * @@ -508,6 +507,7 @@ class RegNetModel(BaseModel): class Header(BaseModel): + model_config = ConfigDict(protected_namespaces=()) name: str schema_name: str schema_url: str = Field(..., alias='schema') diff --git a/mira/modeling/amr/utils.py b/mira/modeling/amr/utils.py index ef975b713..162c0797c 100644 --- a/mira/modeling/amr/utils.py +++ b/mira/modeling/amr/utils.py @@ -4,7 +4,7 @@ def add_metadata_annotations(metadata, model): return annotations_subset = { k: (str(v) if k in ["time_start", "time_end"] and v is not None else v) - for k, v in model.template_model.annotations.dict().items() + for k, v in model.template_model.annotations.model_dump().items() if k not in ["name", "description"] # name and description already have a privileged place # in the petrinet schema so don't get added again From fddeeb2fedf977f69ae6881c91c3a9db453a87c7 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Thu, 12 Sep 2024 09:22:48 -0400 Subject: [PATCH 23/32] Remove deprecated json_encoder from ConfigDict for serializing SympyExprStr objects. Use updated field serializer decorator --- mira/metamodel/comparison.py | 10 ++-------- mira/metamodel/template_model.py | 29 +++++++++++++---------------- mira/metamodel/templates.py | 24 +++++++++--------------- mira/metamodel/units.py | 18 ++++++------------ tests/test_model_api.py | 8 +++++--- 5 files changed, 35 insertions(+), 54 deletions(-) diff --git a/mira/metamodel/comparison.py b/mira/metamodel/comparison.py index a92acfcb4..5fafcac5c 100644 --- a/mira/metamodel/comparison.py +++ b/mira/metamodel/comparison.py @@ -74,14 +74,8 @@ class IntraModelEdge(DataEdge): class ModelComparisonGraphdata(BaseModel): """A data structure holding a graph representation of TemplateModel delta""" - # TODO[pydantic]: The following keys were removed: `json_encoders`. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ - SympyExprStr: lambda e: str(e), - }, json_decoders={ - SympyExprStr: lambda e: safe_parse_expr(e), - Template: lambda t: Template.from_json(data=t), - }) + + model_config = ConfigDict(arbitrary_types_allowed=True) template_models: Dict[int, TemplateModel] = Field( ..., description="A mapping of template model keys to template models" diff --git a/mira/metamodel/template_model.py b/mira/metamodel/template_model.py index 485b82afd..8570ced48 100644 --- a/mira/metamodel/template_model.py +++ b/mira/metamodel/template_model.py @@ -20,7 +20,7 @@ import networkx as nx import sympy import mira.metamodel.io -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_serializer from .templates import * from .units import Unit from .utils import safe_parse_expr, SympyExprStr @@ -36,11 +36,8 @@ class Initial(BaseModel): expression: SympyExprStr = Field( description="The expression for the initial." ) - # TODO[pydantic]: The following keys were removed: `json_encoders`. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ - SympyExprStr: lambda e: str(e), - }, json_decoders={SympyExprStr: lambda e: sympy.parse_expr(e)}) + + model_config = ConfigDict(arbitrary_types_allowed=True) @classmethod def from_json(cls, data: Dict[str, Any], locals_dict=None) -> "Initial": @@ -68,6 +65,10 @@ def from_json(cls, data: Dict[str, Any], locals_dict=None) -> "Initial": expression = safe_parse_expr(expression_str, local_dict=locals_dict) return cls(concept=concept, expression=SympyExprStr(expression)) + @field_serializer('expression') + def serialize_expression(self, expression): + return str(expression) + def substitute_parameter(self, name, value): """ Substitute a parameter value into the initial expression. @@ -135,16 +136,17 @@ class Observable(Concept): readout is not defined as a state variable but is rather a function of state variables. """ - # TODO[pydantic]: The following keys were removed: `json_encoders`. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ - SympyExprStr: lambda e: str(e), - }, json_decoders={SympyExprStr: lambda e: safe_parse_expr(e)}) + + model_config = ConfigDict(arbitrary_types_allowed=True) expression: SympyExprStr = Field( description="The expression for the observable." ) + @field_serializer('expression') + def serialize_expression(self, expression): + return str(expression) + def substitute_parameter(self, name, value): """ Substitute a parameter value into the observable expression. @@ -367,11 +369,6 @@ class TemplateModel(BaseModel): description="A structure containing time-related annotations. " "Note that all annotations are optional.", ) - # TODO[pydantic]: The following keys were removed: `json_encoders`. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict(json_encoders={ - SympyExprStr: lambda e: str(e), - }, json_decoders={SympyExprStr: lambda e: safe_parse_expr(e)}) def get_parameters_from_rate_law(self, rate_law) -> Set[str]: """Given a rate law, find its elements that are model parameters. diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index f0d292d48..7e8ce6c59 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -62,7 +62,7 @@ import networkx as nx import pydantic import sympy -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_serializer try: from typing import Annotated # py39+ @@ -128,13 +128,8 @@ class Concept(BaseModel): None, description="The units of the concept." ) _base_name: str = pydantic.PrivateAttr(None) - # TODO[pydantic]: The following keys were removed: `json_encoders`. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ - SympyExprStr: lambda e: str(e), - }, json_decoders={ - SympyExprStr: lambda e: sympy.parse_expr(e) - }) + + model_config = ConfigDict(arbitrary_types_allowed=True) def __eq__(self, other): if isinstance(other, Concept): @@ -399,13 +394,8 @@ def from_json(cls, data) -> "Concept": class Template(BaseModel): """The Template is a parent class for model processes""" - # TODO[pydantic]: The following keys were removed: `json_encoders`. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict(arbitrary_types_allowed=True, json_encoders={ - SympyExprStr: lambda e: str(e), - }, json_decoders={ - SympyExprStr: lambda e: safe_parse_expr(e) - }) + + model_config = ConfigDict(arbitrary_types_allowed=True) rate_law: Optional[SympyExprStr] = Field( default=None, description="The rate law for the template." @@ -466,6 +456,10 @@ def from_json(cls, data, rate_symbols=None) -> "Template": if k not in {'rate_law', 'type'}}, rate_law=rate) + @field_serializer('rate_law') + def serialize_expression(self, rate_law): + return str(rate_law) + def is_equal_to(self, other: "Template", with_context: bool = False, config: Config = None) -> bool: """Check if this template is equal to another template diff --git a/mira/metamodel/units.py b/mira/metamodel/units.py index ac1378290..31c2af9fa 100644 --- a/mira/metamodel/units.py +++ b/mira/metamodel/units.py @@ -14,7 +14,7 @@ from typing import Dict, Any import sympy -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_serializer from .utils import SympyExprStr @@ -34,17 +34,7 @@ def load_units(): class Unit(BaseModel): """A unit of measurement.""" - # TODO[pydantic]: The following keys were removed: `json_encoders`. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict( - arbitrary_types_allowed=True, - json_encoders={ - SympyExprStr: lambda e: str(e), - }, - #json_decoders={ - # SympyExprStr: lambda e: sympy.parse_expr(e) - #} - ) + model_config = ConfigDict(arbitrary_types_allowed=True) expression: SympyExprStr = Field( description="The expression for the unit." @@ -66,6 +56,10 @@ def model_validate(cls, obj): obj['expression'] = SympyExprStr(obj['expression']) return super().model_validate(obj) + @field_serializer('expression') + def serialize_expression(self, expression): + return str(expression) + person_units = Unit(expression=sympy.Symbol('person')) day_units = Unit(expression=sympy.Symbol('day')) diff --git a/tests/test_model_api.py b/tests/test_model_api.py index 8c34a995b..c799cd8f9 100644 --- a/tests/test_model_api.py +++ b/tests/test_model_api.py @@ -586,16 +586,18 @@ def test_n_way_comparison_askenet(self): "exclude_defaults": True, "exclude_unset": True, "exclude_none": True, + # This key is outdated in Pydantic2 # "skip_defaults": True, } # Compare the ModelComparisonResponse models - assert local_response == resp_model # If assertion fails the diff is printed + # If assertion fails the diff is printed + assert local_response.model_dump() == resp_model.model_dump() local_sorted_str = sorted_json_str( - json.loads(local_response.model_dump_json(**dict_options)), + json.loads(local_response.model_dump_json()), skip_empty=True ) resp_sorted_str = sorted_json_str( - json.loads(resp_model.model_dump_json(**dict_options)), + json.loads(resp_model.model_dump_json()), skip_empty=True ) self.assertEqual(local_sorted_str, resp_sorted_str) From 052360e650d765be6a470089751c7f5ac145348f Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Thu, 12 Sep 2024 09:48:07 -0400 Subject: [PATCH 24/32] Update deprecated json method in test --- tests/test_model_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_model_api.py b/tests/test_model_api.py index c799cd8f9..4aa65425e 100644 --- a/tests/test_model_api.py +++ b/tests/test_model_api.py @@ -282,7 +282,7 @@ def test_stratify_observable_api(self): structure=[], cartesian_control=True) - query_json = {"template_model": json.loads(tm.json())} + query_json = {"template_model": json.loads(tm.model_dump_json())} query_json.update(strata_options) response = self.client.post("/api/stratify", json=query_json) From 66ab8752c9f6f677be92066c47d77ed720525618 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Thu, 12 Sep 2024 10:18:28 -0400 Subject: [PATCH 25/32] Replace deprecated json method in test_model_api --- tests/test_model_api.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_model_api.py b/tests/test_model_api.py index 4aa65425e..e5f51bec0 100644 --- a/tests/test_model_api.py +++ b/tests/test_model_api.py @@ -153,7 +153,8 @@ def test_petri(self): def test_petri_parameterized(self): response = self.client.post( - "/api/to_petrinet_acsets", json=json.loads(sir_parameterized.json()) + "/api/to_petrinet_acsets", json=json.loads( + sir_parameterized.model_dump_json()) ) self.assertEqual(200, response.status_code, msg=response.content) @@ -163,7 +164,8 @@ def test_petri_distribution(self): parameters={'minimum': 0.01, 'maximum': 0.5}) sir_distribution.parameters['beta'].distribution = distr response = self.client.post( - "/api/to_petrinet_acsets", json=json.loads(sir_distribution.json()) + "/api/to_petrinet_acsets", json=json.loads( + sir_distribution.model_dump_json()) ) pm = response.json() assert (pm['T'][0]['tprop']['parameter_distribution'] == @@ -209,7 +211,8 @@ def test_askenet_to_template_model_no_sympy(self): self.assertIsInstance(template_model, TemplateModel) def test_askenet_from_template_model(self): - response = self.client.post("/api/to_petrinet", json=json.loads(sir_parameterized.json())) + response = self.client.post("/api/to_petrinet", json=json.loads( + sir_parameterized.model_dump_json())) self.assertEqual(200, response.status_code, msg=response.content) template_model = template_model_from_amr_json(response.json()) self.assertIsInstance(template_model, TemplateModel) @@ -609,7 +612,7 @@ def test_counts_to_dimensionless_mira(self): response = self.client.post( "/api/counts_to_dimensionless_mira", json={ - "model": json.loads(sir_parameterized_init.json()), + "model": json.loads(sir_parameterized_init.model_dump_json()), "counts_unit": "person", "norm_factor": sir_init_val_norm, }, From 074552b0561b80ea8f7d53961adef2bfead73fb3 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Thu, 12 Sep 2024 12:34:20 -0400 Subject: [PATCH 26/32] Serialize template rate laws to be None if the rate law doesn't exist --- mira/metamodel/templates.py | 2 +- tests/test_model_api.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index 7e8ce6c59..f5d4a2097 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -458,7 +458,7 @@ def from_json(cls, data, rate_symbols=None) -> "Template": @field_serializer('rate_law') def serialize_expression(self, rate_law): - return str(rate_law) + return str(rate_law) if rate_law is not None else None def is_equal_to(self, other: "Template", with_context: bool = False, config: Config = None) -> bool: diff --git a/tests/test_model_api.py b/tests/test_model_api.py index e5f51bec0..b8a396110 100644 --- a/tests/test_model_api.py +++ b/tests/test_model_api.py @@ -589,18 +589,19 @@ def test_n_way_comparison_askenet(self): "exclude_defaults": True, "exclude_unset": True, "exclude_none": True, - # This key is outdated in Pydantic2 - # "skip_defaults": True, } # Compare the ModelComparisonResponse models # If assertion fails the diff is printed + # Use model dump as Pydantic objects have an attribute + # "model_fields_set" that contain explicitly set attributes even if + # are None assert local_response.model_dump() == resp_model.model_dump() local_sorted_str = sorted_json_str( - json.loads(local_response.model_dump_json()), + json.loads(local_response.model_dump_json(**dict_options)), skip_empty=True ) resp_sorted_str = sorted_json_str( - json.loads(resp_model.model_dump_json()), + json.loads(resp_model.model_dump_json(**dict_options)), skip_empty=True ) self.assertEqual(local_sorted_str, resp_sorted_str) From 6ca1149e27302fb55aef67f5290ae5cea639dc31 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Thu, 12 Sep 2024 14:02:12 -0400 Subject: [PATCH 27/32] Update field_serializer decorator arguments to not serialize if rate_law is None for templates --- mira/metamodel/templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index f5d4a2097..82f0a6b6f 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -456,9 +456,9 @@ def from_json(cls, data, rate_symbols=None) -> "Template": if k not in {'rate_law', 'type'}}, rate_law=rate) - @field_serializer('rate_law') + @field_serializer('rate_law', when_used="unless-none") def serialize_expression(self, rate_law): - return str(rate_law) if rate_law is not None else None + return str(rate_law) def is_equal_to(self, other: "Template", with_context: bool = False, config: Config = None) -> bool: From 7cbf2dfc01fda126bea360e43785af6eb68b210d Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 23 Sep 2024 13:11:52 -0400 Subject: [PATCH 28/32] Adjust test for concept equality testing --- mira/metamodel/templates.py | 11 ----------- tests/test_ops.py | 34 ++++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/mira/metamodel/templates.py b/mira/metamodel/templates.py index 82f0a6b6f..df4778223 100644 --- a/mira/metamodel/templates.py +++ b/mira/metamodel/templates.py @@ -131,17 +131,6 @@ class Concept(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - def __eq__(self, other): - if isinstance(other, Concept): - return (self.name == other.name and - self.display_name == other.display_name and - self.description == other.description and - self.identifiers == other.identifiers and - self.context == other.context and - self.units == other.units) - else: - return False - def with_context(self, do_rename=False, curie_to_name_map=None, inplace=False, **context) -> "Concept": """Return this concept with extra context. diff --git a/tests/test_ops.py b/tests/test_ops.py index 7276e131c..b46c3ea40 100644 --- a/tests/test_ops.py +++ b/tests/test_ops.py @@ -159,19 +159,33 @@ def test_stratify_full(self): self.assertEqual(tm_stratified.parameters, actual.parameters) self.assertTrue(actual.initials['infected_population_vaccinated'].expression.equals( tm_stratified.initials['infected_population_vaccinated'].expression)) - self.assertEqual( + + def one_to_one_concept_mapping(strat_concepts, actual_concepts): + matched_concept_idxs = set() + for strat_concept in strat_concepts: + matched = False + for concept_idx, actual_concept in enumerate(actual_concepts): + if (strat_concept.is_equal_to(actual_concept) and + concept_idx not in matched_concept_idxs): + matched_concept_idxs.add(concept_idx) + matched = True + break + if not matched: + return False + return True + + self.assertTrue(one_to_one_concept_mapping( [t.subject for t in tm_stratified.templates], - [t.subject for t in actual.templates], - ) - self.assertEqual( + [t.subject for t in actual.templates])) + + self.assertTrue(one_to_one_concept_mapping( [t.outcome for t in tm_stratified.templates], - [t.outcome for t in actual.templates], - ) - self.assertEqual( + [t.outcome for t in actual.templates])) + + self.assertTrue(one_to_one_concept_mapping( [t.controller for t in tm_stratified.templates], - [t.controller for t in actual.templates], - ) - self.assertEqual(tm_stratified.templates, actual.templates) + [t.controller for t in actual.templates])) + def test_stratify(self): """Test stratifying a template model by labels.""" From a866cf3c31a92ef0c2713342670de4fc345901a9 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 23 Sep 2024 14:10:56 -0400 Subject: [PATCH 29/32] Replace deprecated usage of dict() with model_dump() in notebooks --- notebooks/ensemble/ensemble.ipynb | 26 ++--- notebooks/metamodel_intro.ipynb | 102 +++++++++++++++-- notebooks/model_api.ipynb | 179 +++++++++++++++++++----------- 3 files changed, 216 insertions(+), 91 deletions(-) diff --git a/notebooks/ensemble/ensemble.ipynb b/notebooks/ensemble/ensemble.ipynb index 3792b607d..70091428b 100644 --- a/notebooks/ensemble/ensemble.ipynb +++ b/notebooks/ensemble/ensemble.ipynb @@ -76,9 +76,7 @@ "cell_type": "code", "execution_count": 2, "id": "3c0e6c34", - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "name": "stderr", @@ -786,9 +784,9 @@ "outputs": [], "source": [ "model.observables = {\n", - " 'Cases': Observable(**cases.dict(), expression=Symbol('Infected_reported')),\n", - " 'Hospitalizations': Observable(**hospitalizations.dict(), expression=Symbol('h')*Symbol('Infected_reported')),\n", - " 'Deaths': Observable(**deaths.dict(), expression=Symbol('Deceased'))\n", + " 'Cases': Observable(**cases.model_dump(), expression=Symbol('Infected_reported')),\n", + " 'Hospitalizations': Observable(**hospitalizations.model_dump(), expression=Symbol('h')*Symbol('Infected_reported')),\n", + " 'Deaths': Observable(**deaths.model_dump(), expression=Symbol('Deceased'))\n", "}" ] }, @@ -972,9 +970,9 @@ "outputs": [], "source": [ "model.observables = {\n", - " 'Cases': Observable(**cases.dict(), expression=Symbol('Diagnosed')+Symbol('Recognized')+Symbol('Threatened')),\n", - " 'Hospitalizations': Observable(**hospitalizations.dict(), expression=Symbol('Recognized')+Symbol('Threatened')),\n", - " 'Deaths': Observable(**deaths.dict(), expression=Symbol('Extinct'))\n", + " 'Cases': Observable(**cases.model_dump(), expression=Symbol('Diagnosed')+Symbol('Recognized')+Symbol('Threatened')),\n", + " 'Hospitalizations': Observable(**hospitalizations.model_dump(), expression=Symbol('Recognized')+Symbol('Threatened')),\n", + " 'Deaths': Observable(**deaths.model_dump(), expression=Symbol('Extinct'))\n", "}" ] }, @@ -1015,9 +1013,7 @@ "cell_type": "code", "execution_count": 31, "id": "677b8074", - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -1140,9 +1136,9 @@ "outputs": [], "source": [ "model.observables = {\n", - " 'Cases': Observable(**cases.dict(), expression=Symbol('Infectious')),\n", - " 'Hospitalizations': Observable(**hospitalizations.dict(), expression=Symbol('Hospitalized')),\n", - " 'Deaths': Observable(**deaths.dict(), expression=Symbol('Deceased'))\n", + " 'Cases': Observable(**cases.model_dump(), expression=Symbol('Infectious')),\n", + " 'Hospitalizations': Observable(**hospitalizations.model_dump(), expression=Symbol('Hospitalized')),\n", + " 'Deaths': Observable(**deaths.model_dump(), expression=Symbol('Deceased'))\n", "}" ] }, diff --git a/notebooks/metamodel_intro.ipynb b/notebooks/metamodel_intro.ipynb index 31de33d9a..6cd1546c6 100644 --- a/notebooks/metamodel_intro.ipynb +++ b/notebooks/metamodel_intro.ipynb @@ -41,7 +41,20 @@ "outputs": [ { "data": { - "text/plain": "{'rate_law': None,\n 'type': 'ControlledConversion',\n 'controller': {'name': 'infected population',\n 'identifiers': {'ido': '0000511'},\n 'context': {}},\n 'subject': {'name': 'susceptible population',\n 'identifiers': {'ido': '0000514'},\n 'context': {}},\n 'outcome': {'name': 'infected population',\n 'identifiers': {'ido': '0000511'},\n 'context': {}},\n 'provenance': []}" + "text/plain": [ + "{'rate_law': None,\n", + " 'type': 'ControlledConversion',\n", + " 'controller': {'name': 'infected population',\n", + " 'identifiers': {'ido': '0000511'},\n", + " 'context': {}},\n", + " 'subject': {'name': 'susceptible population',\n", + " 'identifiers': {'ido': '0000514'},\n", + " 'context': {}},\n", + " 'outcome': {'name': 'infected population',\n", + " 'identifiers': {'ido': '0000511'},\n", + " 'context': {}},\n", + " 'provenance': []}" + ] }, "execution_count": 16, "metadata": {}, @@ -49,7 +62,7 @@ } ], "source": [ - "t1.dict()" + "t1.model_dump()" ] }, { @@ -61,7 +74,9 @@ "outputs": [ { "data": { - "text/plain": "ControlledConversion(rate_law=None, type='ControlledConversion', controller=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), subject=Concept(name='susceptible population', identifiers={'ido': '0000514'}, context={}), outcome=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), provenance=[])" + "text/plain": [ + "ControlledConversion(rate_law=None, type='ControlledConversion', controller=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), subject=Concept(name='susceptible population', identifiers={'ido': '0000514'}, context={}), outcome=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), provenance=[])" + ] }, "execution_count": 28, "metadata": {}, @@ -69,7 +84,7 @@ } ], "source": [ - "Template.from_json(t1.dict())" + "Template.from_json(t1.model_dump())" ] }, { @@ -217,7 +232,70 @@ "outputs": [ { "data": { - "text/plain": "{'templates': [{'rate_law': None,\n 'type': 'ControlledConversion',\n 'controller': {'name': 'infected population',\n 'identifiers': {'ido': '0000511'},\n 'context': {}},\n 'subject': {'name': 'susceptible population',\n 'identifiers': {'ido': '0000514'},\n 'context': {}},\n 'outcome': {'name': 'exposed population',\n 'identifiers': {'genepio': '0001538'},\n 'context': {}},\n 'provenance': []},\n {'rate_law': None,\n 'type': 'NaturalConversion',\n 'subject': {'name': 'exposed population',\n 'identifiers': {'genepio': '0001538'},\n 'context': {}},\n 'outcome': {'name': 'infected population',\n 'identifiers': {'ido': '0000511'},\n 'context': {}},\n 'provenance': []},\n {'rate_law': None,\n 'type': 'NaturalConversion',\n 'subject': {'name': 'infected population',\n 'identifiers': {'ido': '0000511'},\n 'context': {}},\n 'outcome': {'name': 'deceased population',\n 'identifiers': {'ncit': 'C28554'},\n 'context': {}},\n 'provenance': []},\n {'rate_law': None,\n 'type': 'NaturalConversion',\n 'subject': {'name': 'infected population',\n 'identifiers': {'ido': '0000511'},\n 'context': {}},\n 'outcome': {'name': 'immune population',\n 'identifiers': {'ido': '0000592'},\n 'context': {}},\n 'provenance': []},\n {'rate_law': None,\n 'type': 'ControlledConversion',\n 'controller': {'name': 'exposed population',\n 'identifiers': {'genepio': '0001538'},\n 'context': {}},\n 'subject': {'name': 'susceptible population',\n 'identifiers': {'ido': '0000514'},\n 'context': {}},\n 'outcome': {'name': 'exposed population',\n 'identifiers': {'genepio': '0001538'},\n 'context': {}},\n 'provenance': []},\n {'rate_law': None,\n 'type': 'NaturalConversion',\n 'subject': {'name': 'immune population',\n 'identifiers': {'ido': '0000592'},\n 'context': {}},\n 'outcome': {'name': 'susceptible population',\n 'identifiers': {'ido': '0000514'},\n 'context': {}},\n 'provenance': []}],\n 'parameters': {},\n 'initials': {}}" + "text/plain": [ + "{'templates': [{'rate_law': None,\n", + " 'type': 'ControlledConversion',\n", + " 'controller': {'name': 'infected population',\n", + " 'identifiers': {'ido': '0000511'},\n", + " 'context': {}},\n", + " 'subject': {'name': 'susceptible population',\n", + " 'identifiers': {'ido': '0000514'},\n", + " 'context': {}},\n", + " 'outcome': {'name': 'exposed population',\n", + " 'identifiers': {'genepio': '0001538'},\n", + " 'context': {}},\n", + " 'provenance': []},\n", + " {'rate_law': None,\n", + " 'type': 'NaturalConversion',\n", + " 'subject': {'name': 'exposed population',\n", + " 'identifiers': {'genepio': '0001538'},\n", + " 'context': {}},\n", + " 'outcome': {'name': 'infected population',\n", + " 'identifiers': {'ido': '0000511'},\n", + " 'context': {}},\n", + " 'provenance': []},\n", + " {'rate_law': None,\n", + " 'type': 'NaturalConversion',\n", + " 'subject': {'name': 'infected population',\n", + " 'identifiers': {'ido': '0000511'},\n", + " 'context': {}},\n", + " 'outcome': {'name': 'deceased population',\n", + " 'identifiers': {'ncit': 'C28554'},\n", + " 'context': {}},\n", + " 'provenance': []},\n", + " {'rate_law': None,\n", + " 'type': 'NaturalConversion',\n", + " 'subject': {'name': 'infected population',\n", + " 'identifiers': {'ido': '0000511'},\n", + " 'context': {}},\n", + " 'outcome': {'name': 'immune population',\n", + " 'identifiers': {'ido': '0000592'},\n", + " 'context': {}},\n", + " 'provenance': []},\n", + " {'rate_law': None,\n", + " 'type': 'ControlledConversion',\n", + " 'controller': {'name': 'exposed population',\n", + " 'identifiers': {'genepio': '0001538'},\n", + " 'context': {}},\n", + " 'subject': {'name': 'susceptible population',\n", + " 'identifiers': {'ido': '0000514'},\n", + " 'context': {}},\n", + " 'outcome': {'name': 'exposed population',\n", + " 'identifiers': {'genepio': '0001538'},\n", + " 'context': {}},\n", + " 'provenance': []},\n", + " {'rate_law': None,\n", + " 'type': 'NaturalConversion',\n", + " 'subject': {'name': 'immune population',\n", + " 'identifiers': {'ido': '0000592'},\n", + " 'context': {}},\n", + " 'outcome': {'name': 'susceptible population',\n", + " 'identifiers': {'ido': '0000514'},\n", + " 'context': {}},\n", + " 'provenance': []}],\n", + " 'parameters': {},\n", + " 'initials': {}}" + ] }, "execution_count": 27, "metadata": {}, @@ -225,7 +303,7 @@ } ], "source": [ - "M4.dict()" + "M4.model_dump()" ] }, { @@ -237,7 +315,9 @@ "outputs": [ { "data": { - "text/plain": "TemplateModel(templates=[ControlledConversion(rate_law=None, type='ControlledConversion', controller=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), subject=Concept(name='susceptible population', identifiers={'ido': '0000514'}, context={}), outcome=Concept(name='exposed population', identifiers={'genepio': '0001538'}, context={}), provenance=[]), NaturalConversion(rate_law=None, type='NaturalConversion', subject=Concept(name='exposed population', identifiers={'genepio': '0001538'}, context={}), outcome=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), provenance=[]), NaturalConversion(rate_law=None, type='NaturalConversion', subject=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), outcome=Concept(name='deceased population', identifiers={'ncit': 'C28554'}, context={}), provenance=[]), NaturalConversion(rate_law=None, type='NaturalConversion', subject=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), outcome=Concept(name='immune population', identifiers={'ido': '0000592'}, context={}), provenance=[]), ControlledConversion(rate_law=None, type='ControlledConversion', controller=Concept(name='exposed population', identifiers={'genepio': '0001538'}, context={}), subject=Concept(name='susceptible population', identifiers={'ido': '0000514'}, context={}), outcome=Concept(name='exposed population', identifiers={'genepio': '0001538'}, context={}), provenance=[]), NaturalConversion(rate_law=None, type='NaturalConversion', subject=Concept(name='immune population', identifiers={'ido': '0000592'}, context={}), outcome=Concept(name='susceptible population', identifiers={'ido': '0000514'}, context={}), provenance=[])], parameters={}, initials={})" + "text/plain": [ + "TemplateModel(templates=[ControlledConversion(rate_law=None, type='ControlledConversion', controller=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), subject=Concept(name='susceptible population', identifiers={'ido': '0000514'}, context={}), outcome=Concept(name='exposed population', identifiers={'genepio': '0001538'}, context={}), provenance=[]), NaturalConversion(rate_law=None, type='NaturalConversion', subject=Concept(name='exposed population', identifiers={'genepio': '0001538'}, context={}), outcome=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), provenance=[]), NaturalConversion(rate_law=None, type='NaturalConversion', subject=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), outcome=Concept(name='deceased population', identifiers={'ncit': 'C28554'}, context={}), provenance=[]), NaturalConversion(rate_law=None, type='NaturalConversion', subject=Concept(name='infected population', identifiers={'ido': '0000511'}, context={}), outcome=Concept(name='immune population', identifiers={'ido': '0000592'}, context={}), provenance=[]), ControlledConversion(rate_law=None, type='ControlledConversion', controller=Concept(name='exposed population', identifiers={'genepio': '0001538'}, context={}), subject=Concept(name='susceptible population', identifiers={'ido': '0000514'}, context={}), outcome=Concept(name='exposed population', identifiers={'genepio': '0001538'}, context={}), provenance=[]), NaturalConversion(rate_law=None, type='NaturalConversion', subject=Concept(name='immune population', identifiers={'ido': '0000592'}, context={}), outcome=Concept(name='susceptible population', identifiers={'ido': '0000514'}, context={}), provenance=[])], parameters={}, initials={})" + ] }, "execution_count": 14, "metadata": {}, @@ -245,13 +325,13 @@ } ], "source": [ - "TemplateModel.from_json(M4.dict())" + "TemplateModel.from_json(M4.model_dump())" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -265,9 +345,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.9" + "version": "3.10.12" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/notebooks/model_api.ipynb b/notebooks/model_api.ipynb index 51041b752..87d293663 100644 --- a/notebooks/model_api.ipynb +++ b/notebooks/model_api.ipynb @@ -44,7 +44,7 @@ ")\n", "natural_conversion = NaturalConversion(subject=infected, outcome=immune)\n", "sir_template_model = TemplateModel(templates=[controlled_conversion, natural_conversion])\n", - "sir_template_model_dict = sir_template_model.dict()\n", + "sir_template_model_dict = sir_template_model.m()\n", "print(sir_template_model.json())" ] }, @@ -61,7 +61,10 @@ "cell_type": "code", "execution_count": 2, "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [ { @@ -106,17 +109,26 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "## Biomodels Model\n", "The `/api/biomodels/` endpoint returns a `TemplateModel` json of the biomodels model." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 4, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -129,24 +141,30 @@ "source": [ "res = requests.get(rest_url + \"/api/biomodels/BIOMD0000000956\")\n", "print(res.json())" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "## Bilayer Endpoints\n", "The `/api/model_to_bilayer` and `/api/bilayer_to_model` endpoints can translate between a `TemplateModel` json and a `Bilayer`" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 5, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -160,14 +178,17 @@ "# Get a bilayer json representation from a TemplateModel json\n", "res = requests.post(rest_url + \"/api/model_to_bilayer\", json=sir_template_model_dict)\n", "print(res.json())" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 6, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -182,26 +203,32 @@ "bilayer_json = res.json()\n", "res = requests.post(rest_url + \"/api/bilayer_to_model\", json=bilayer_json)\n", "print(res.json())" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "## SBML XML To Model\n", "For the `/api/sbml_xml_to_model` endpoint, you can provide an SBML model in the form of a string of the XML and get back a model.\n", "\n", "First, get an XML string. Here, done by the `get_sbml_model` function from the `biomodels` submodule in MIRA:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 7, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -217,23 +244,29 @@ "from mira.sources.biomodels import get_sbml_model\n", "biomd951_xml_str = get_sbml_model(\"BIOMD0000000956\")\n", "print(biomd951_xml_str[:250])" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "Next, submit this string to the endpoint and get the corresponding model back:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 8, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -246,10 +279,7 @@ "source": [ "res = requests.post(rest_url + \"/api/sbml_xml_to_model\", json={\"xml_string\": biomd951_xml_str})\n", "print(res.json())" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", @@ -341,7 +371,9 @@ { "data": { "image/png": "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\n", - "text/plain": "" + "text/plain": [ + "" + ] }, "execution_count": 10, "metadata": {}, @@ -369,33 +401,47 @@ { "cell_type": "code", "execution_count": 11, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "# Add a location to the templates that are not NaturalConversion (Note that this has no meaning outside as an illustration in this example)\n", "templates_w_context = [templ.with_context(location=\"geonames:5128581\") if not isinstance(templ, NaturalConversion) else templ for templ in sir_template_model.templates]\n", "sir_w_context = TemplateModel(templates=templates_w_context, parameters=sir_template_model.parameters, initials=sir_template_model.initials)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "Submit the two models to `/api/models_to_delta_image` to get an image of the delta graph:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 12, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAC9QAAAEFCAYAAAC46k5iAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nOzdd3RUZf7H8fek94SahIBSpChiooCowEovEoosrojiogviz4KIuIptPWJZlVXRlRV11UVFUXYJUgQVsdCLEhVp0iEJJISEBJKQ8vz+uMyEYSYwCUkmMZ/XOfdM8sz3Pvf73BY453ufazPGGERERERERERERERERERERERERERERERE6pY9Pt7OQERERERERERERERERERERERERERERETEG1RQLyIiIiIiIiIiIiIiIiIiIiIiIiIiIiJ1kgrqRURERERERERERERERERERERERERERKRO8vN2AiLVKT8f8vKc27KywJhzx50t1tcX/P2d2319ISLCtY+wMNdYPz8IDz93nIiIiIiIiIiIiIiIiIiIiIiIiIiIiFQeFdRLjVJQYBWt25djx6zPwkLIzS0tdM/NtdqysqCoyIo7eRKOH4cTJ6x+jh2zvrPH5OR4e3QVExlpFdxHRkJAAISGQkgIBAZaBfu+vlCvXmlRflAQBAeXFuSfvn5UlPPipzuAiIiIiIiIiIiIiIiIiIiIiIiIiIjUYTZjzpxvW6TiCgshM9O5KD4rC7Kz4ehR1/bTv8/Kcj8rPFiF4WFhVqF4UFBpsXhUlFVQfnqxuT0mPNwqGLcXjkdElMacztMZ46G0zzNjS0qs5XQFBVZx/5k8nRE/J8d6EODoUSguth4QsPd5+kMDxcVWjP2hAXvM8ePWQwb2GHdCQ12L7M9c7IX49eo5t9evDz4+7vsVERERERERERERERERERERERERERGpBfaooF7OKT8f0tMhNRUOH7Z+TkmxPg8fdm5PT3dd32ZzX5BdVuH2mW2RkdU/5t8bY87+IMO5luxs1z59faFRI2uJiYHoaGjcuPTnRo0gNtZqa9TI9aEFERERERERERERERERERERERERERERL1NBfV127Bjs2wd798LBg5CW5lw4f/iw1XZmMXVoaGmhdOPGzkXTTZpAgwYqiP89OvMNA4cPw6FD7h+2OHzYehDjdPbie3fnTFwcXHABXHih9YYBERERERERERERERERERERERERERGRaqCC+t+roiKryHnvXqtoft8+2L+/tIB+3z7nQvmICKuwuVEja3bxmJjSmcdP/zk6GkJCvDcuqT1yclzfZJCebhXhn/nziROl6zVqBM2aWQX29iL703+PibHeeiAiIiIiIiIiIiIiIiIiIiIiIiIiInKeVFBfWxUWwu7dsGOHVSC/f7+17N1rLampVlE9gL+/NQN4s2bQvHlpcXKzZlax8gUXQHi4V4cjdVxmZulDH3v2lJ7P9gdAUlOhpMSKDQyEpk1Lz2H7OX3hhdC6tdXu4+PN0YiIiIiIiIiIiIiIiIiIiIiIiIiISC2hgvqa7uhR2LwZfv0Vdu0qXTZvhvx8KyYoyJpdvmVLa4mNdf79ggvAz8+74xA5H4WF1oz2qaml10BKSunvO3dCVpYVGxBgFdy3bAmXXALt25deCy1aaHZ7ERERERERERERERERERERERERERFxUEF9TZCZCdu3Oy87dlifJ05YMVFR0KaNNQN327alP7durdnlRcC6jnbsgG3bnK+jHTvg+HErJirKumbatLGuI/vPuo5EREREREREREREREREREREREREROokFdRXp5wc+Okna0lOhp9/top/jxyxvg8MLC3wtRf52gt/GzXybu4itdn+/aUPqZxedL9njzX7PVhvdWjXDjp0gMsug/h4a3b7oCCvpi4iIiIiIiIiIiIiIiIiIiIiIiIiIlVHBfVVwRjYvRs2bXIuoN+92/ouKsoq2O3QAS6+uLR4/oILwMfH29mL1B2FhdZ1aZ/RfutW67rdvNl6O4Sfn3V92gvs4+Otn+PivJ25iIiIiIiIiIiIiIiIiIiIiIiIiIhUAhXUn6/cXGu2682bYeNG+PVX+PHH0lnnY2OhY0drad8eLrnEWmw27+YtImeXkmJd0/brevNm2LKl9KGY9u1Lr2n7NR4c7O2sRURERERERERERERERERERERERESkHFRQXx75+dbs1evWWcv69bBjh1VgGxlpzTh/+kzWl14KoaHezlpEKsvRo9bbJk5/88TmzZCXZ81mf8klcOWV0KULdO5sFdz7+Xk7axERERERERERERERERERERERERERKYMK6s9m+3ZYvbq0gD45GQoLoX59q2j2yivh8sut4vkWLbydrYh4Q3Gxda/46SfYsMG6V2zcCMePWw/UXHFF6f2ia1eIi/N2xiIiIiIiIiIiIiIiIiIiIiIiIiIicooK6u2Ki2HrVli5ElasgG+/hX37wN8fWreGbt2sYtiOHa1ZqG02b2cs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fOJx31I3hYXBjBnw+efw3XfezsbZc889R6tWrbj99tv5/PPPyc7OJjMzk5kzZ/LUU08xbdq0cr/doTblUtXbvOKKK9i+fTv79+9n9erV7Nq1i+7du5crvxYtWjBx4kQWLlxITk4O27dvZ9SoUaSmpjJ9+nSio6MrnF+lyv4VvhsOcYnwh/9ZszP/jt1555389NNPFBQUcPjwYV544QWMMfQ645VG/fr1IyUlhX/+85/k5uayc+dO7rvvvjJnYPekX/t5cf/997No0SJycnI4cOAAd911F6mpqY6C+oqc35GRkTzyyCOsXr2a48ePs2HDBm655RYCAgKYPn26U+z5nN+hoaHcc889rF279pzbcacyr11Px1yR67E8+7O858qZfH196dGjB2lpabz44otkZGSQl5fH8uXLeeONN9yuU5FjWKvuSzVF1GXQdwXkH4bvb4CSQm9nJCIiIiIiIiIiIiIiIiIiIuLMuHHihDFxccZMmODuWxFnjz76qPHz8zMHDx4877569+5tAgMDKyErZ8uXLzeAuffee12+e/LJJw1g5s6dW2PjjTGma9euZv369Z4N2Bhz4sQJx89xcXHG19fXo/Wuu+46c8cdd5j333/fAGbq1Kllxt5www2madOmprCw0OO8pO7q3duYbt2qZ1urV682gMvizpEjR8ykSZNMy5Ytjb+/v2nUqJHp16+f+fLLLx0x8+bNc+nr5ptvNsYY079/f5fvXnzxRZe2Rx991BhjXNoHDRpUrlzcje3RRx8169evd2l/7rnnzPfff+/S/re//a1Stnmu8WzdutV0797dhIaGmmbNmpnXX3+93McyIyPDTJw40bRo0cL4+/ubyMhI079/f7Ns2bKzHh/ArF692vMN7dtnTEXvZYU5xsxvZcwX3Ywpzq9YH7XIpk2bzPjx483FF19sQkJCTP369c1VV11l3nrrLVNSUuIUm5WVZcaOHWtiY2NNcHCw6datm1m/fr3p2LGj4zg99NBD5e73zPMiNjbWjBw50mzfvt0pzpPz2y4+Pt7ExcWZX3/91fTv39+Eh4eb4OBgc+2115oVK1a4xJd1fpd1vzj9vhAXF2fWrVtnevbsacLCwtxu52z3kfKOrSzlHbMn12NF+/b0XDnbfklPTzfjx483zZo1M/7+/iY6OtqMGTPGPPzww47Yjh07VvgYlmc/VPS++buW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4L/Rqmq9zgoc66uK+K6Pqar3ro+BW3Owdqma5/IgOX8efv8djhwpvj1xAgoKwN4eAgJU+Ll3b/D3h8aNVTV6C6mFLSqBTqfeX/oRD4zl50N8vPq95dgx9V79z3/UKAjG79cWLaB5czW1aKEuFHxI6LTyjDUmhBBCCCEeeJqm/mZbtw769avq3gghoPizuH591fZDlM7NzY309HSuXbtmMuQ1wCuvvMJnn33GJ598wiuvvAKoymuBgYHs3bvXpG2rVq04c+YMqampJsvLan+rfd9JH3v37s3mzZu5cOGCyfDKXbt25fvvv+f69es4ODgYlnfo0IHff/+da9eumewzIyODq1ev4uJieiI2ODiYX3/9lcTEROrWrVtmv4UQQgghhBBCCFF9LFsGERGQlla+9qmpqXh6eposCw4OLvMC+9dff51169aZnBtJTU1lypQpfPPNN6SkpODh4UH37t2pXbs277zzjtk2T5w4wbBhw/j111/x8PBg4sSJvP7664btpaenM378eLZu3UpaWhrBwcEsXLiQkSNHcujQIQAiIyPp3bs3ISEhZn0sGasob/8GDBjAhAkTTB47efJkZs2aZbLsl19+4cMPP+THH38kJSUFOzs7AgICGDhwICNHjsTGxsak/eXLl5k1axabN28mKSkJBwcH2rRpQ0REhCF8HhcXZ/Zc9PvWlQj49ujRg6+//tpkWUREBPPnz+f333+nefPmZscE4MqVK8yePZtNmzaRmJiIm5sbQUFBTJgwgbCwsDL7Udox/f3331m6dCl79uwhISEBOzs7mjRpwpAhQxgyZAg6nY5NmzYZKtHrvfTSS3z++ef37LjcTJcu4OsLpeTEH1phYWHs3bu3wheuVBsFBSoUeehQ8XTwoKo87OICjz8OwcFq6tABGjSo6h4LIUSxonzISiwO2etvr/23uGq9jbtpyN41QAXvnRpWbd+rq2vX4MABiI2FuDg1r/8dtn590xBy8+YqPG9pWbV9FuJWcnPh6FEVsNdPhw+ryvag3ttPPqlGV2jTRlW5t7Wt2j5XjjMSqBdCCCGEEIAE6oWojvr1U6P69eypRl175BH192rNmlXdM5Gbm4udnR12dnalDuU9YcIEoqOjmTdvHhEREcDdC9Tfat930kd9oD47Oxs7o+GGn3nmGXbs2GE2THenTp04ePAgmZmZJvu8lV27dvHU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\n", - "text/plain": "" + "text/plain": [ + "" + ] }, "execution_count": 12, "metadata": {}, @@ -403,29 +449,35 @@ } ], "source": [ - "res = requests.post(rest_url + \"/api/models_to_delta_image\", json={\"template_model1\": sir_template_model_dict, \"template_model2\": sir_w_context.dict()})\n", + "res = requests.post(rest_url + \"/api/models_to_delta_image\", json={\"template_model1\": sir_template_model_dict, \"template_model2\": sir_w_context.model_dump()})\n", "with open(\"./delta_graph.png\", \"wb\") as f:\n", " f.write(res.content)\n", "Image(filename=\"./delta_graph.png\")" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "From the image, it can be seen that the NaturalConversion of the two models are equals, while the ControlledConversion in one model is a refinement of the other, in this case because the context was added.\n", "\n", "The other endpoint provides a way to download the graph seen in the image as a node-link data json:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 13, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -436,17 +488,14 @@ } ], "source": [ - "res = requests.post(rest_url + \"/api/models_to_delta_graph\", json={\"template_model1\": sir_template_model_dict, \"template_model2\": sir_w_context.dict()})\n", + "res = requests.post(rest_url + \"/api/models_to_delta_graph\", json={\"template_model1\": sir_template_model_dict, \"template_model2\": sir_w_context.model_dump()})\n", "print(res.json())" - ], - "metadata": { - "collapsed": false - } + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -460,9 +509,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.9" + "version": "3.10.12" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } From 309b52ac5dcd92dff8c5a29de38ce2b3c0dda0e3 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 23 Sep 2024 14:38:45 -0400 Subject: [PATCH 30/32] replace deprecated usage of json() with model_dump_json() for pydantic2 migration --- notebooks/Hackathon Scenario 1.ipynb | 8 ++++---- notebooks/Hackathon Scenario 4.ipynb | 2 +- notebooks/evaluation_2023.01/Scenario2.ipynb | 2 +- notebooks/evaluation_2023.01/Scenario3.ipynb | 8 ++++---- notebooks/model_api.ipynb | 2 +- notebooks/scenarios_2024.08/eval_202301_scenario2.ipynb | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/notebooks/Hackathon Scenario 1.ipynb b/notebooks/Hackathon Scenario 1.ipynb index 7239d2275..3b6a2dce2 100644 --- a/notebooks/Hackathon Scenario 1.ipynb +++ b/notebooks/Hackathon Scenario 1.ipynb @@ -2628,7 +2628,7 @@ } ], "source": [ - "print(tc.model_comparison.json(indent=1))" + "print(tc.model_comparison.model_dump_json(indent=1))" ] }, { @@ -4710,7 +4710,7 @@ } ], "source": [ - "print(tc.model_comparison.json(indent=1))" + "print(tc.model_comparison.model_dump_json(indent=1))" ] }, { @@ -4721,7 +4721,7 @@ "outputs": [], "source": [ "with open('mira_comparison_threeway.json', 'w') as fh:\n", - " fh.write(tc.model_comparison.json(indent=1))" + " fh.write(tc.model_comparison.model_dump_json(indent=1))" ] }, { @@ -4779,7 +4779,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/notebooks/Hackathon Scenario 4.ipynb b/notebooks/Hackathon Scenario 4.ipynb index 3a3b9e0a0..92001f0a8 100644 --- a/notebooks/Hackathon Scenario 4.ipynb +++ b/notebooks/Hackathon Scenario 4.ipynb @@ -74,7 +74,7 @@ "outputs": [], "source": [ "with open('mira_comparison_scenario4.json', 'w') as fh:\n", - " fh.write(tc.model_comparison.json(indent=1))" + " fh.write(tc.model_comparison.model_dump_json(indent=1))" ] }, { diff --git a/notebooks/evaluation_2023.01/Scenario2.ipynb b/notebooks/evaluation_2023.01/Scenario2.ipynb index 55aa154fb..35e781f4c 100644 --- a/notebooks/evaluation_2023.01/Scenario2.ipynb +++ b/notebooks/evaluation_2023.01/Scenario2.ipynb @@ -456,7 +456,7 @@ "outputs": [], "source": [ "with open('scenario2_model_comparison.json', 'w') as fh:\n", - " fh.write(tc.model_comparison.json(indent=1))" + " fh.write(tc.model_comparison.model_dump_json(indent=1))" ] } ], diff --git a/notebooks/evaluation_2023.01/Scenario3.ipynb b/notebooks/evaluation_2023.01/Scenario3.ipynb index e58863ad3..d0c70b703 100644 --- a/notebooks/evaluation_2023.01/Scenario3.ipynb +++ b/notebooks/evaluation_2023.01/Scenario3.ipynb @@ -285,9 +285,9 @@ " model.to_json_file(mname, indent=1)\n", "# Also dump the mira model jsons\n", "with open(\"scenario3_biomd958_mira.json\", \"w\") as f:\n", - " f.write(model_958.json(indent=1))\n", + " f.write(model_958.model_dump_json(indent=1))\n", "with open(\"scenario3_biomd960_mira.json\", \"w\") as f:\n", - " f.write(model_960.json(indent=1))" + " f.write(model_960.model_dump_json(indent=1))" ] }, { @@ -534,12 +534,12 @@ "source": [ "import json\n", "res = {\n", - " 'graph_comparison_data': json.loads(mc.model_comparison.json()),\n", + " 'graph_comparison_data': json.loads(mc.model_comparison.model_dump_json()),\n", " 'similarity_scores': mc.model_comparison.get_similarity_scores(),\n", " 'model_names': model_names\n", "}\n", "res_filtered = {\n", - " 'graph_comparison_data': json.loads(mc_filtered.model_comparison.json()),\n", + " 'graph_comparison_data': json.loads(mc_filtered.model_comparison.model_dump_json()),\n", " 'similarity_scores': mc_filtered.model_comparison.get_similarity_scores(),\n", " 'model_names': model_names_filtered\n", "}" diff --git a/notebooks/model_api.ipynb b/notebooks/model_api.ipynb index 87d293663..386c28730 100644 --- a/notebooks/model_api.ipynb +++ b/notebooks/model_api.ipynb @@ -45,7 +45,7 @@ "natural_conversion = NaturalConversion(subject=infected, outcome=immune)\n", "sir_template_model = TemplateModel(templates=[controlled_conversion, natural_conversion])\n", "sir_template_model_dict = sir_template_model.m()\n", - "print(sir_template_model.json())" + "print(sir_template_model.model_dump_json())" ] }, { diff --git a/notebooks/scenarios_2024.08/eval_202301_scenario2.ipynb b/notebooks/scenarios_2024.08/eval_202301_scenario2.ipynb index 97ff219b9..f577fe3ba 100644 --- a/notebooks/scenarios_2024.08/eval_202301_scenario2.ipynb +++ b/notebooks/scenarios_2024.08/eval_202301_scenario2.ipynb @@ -431,7 +431,7 @@ "outputs": [], "source": [ "with open('eval_202301_scenario2_sidarthe_v_model_comparison.json', 'w') as fh:\n", - " fh.write(tc.model_comparison.json(indent=1))" + " fh.write(tc.model_comparison.model_dump_json(indent=1))" ] } ], @@ -451,7 +451,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.10.12" } }, "nbformat": 4, From f6341e034753c8d4463aa485fd99e69ecf4a4b47 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 23 Sep 2024 14:53:34 -0400 Subject: [PATCH 31/32] Replace deprecated instance of dict with model_dump and don't execute notebooks to produce minimal diff --- notebooks/model_api.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/model_api.ipynb b/notebooks/model_api.ipynb index 386c28730..f10024735 100644 --- a/notebooks/model_api.ipynb +++ b/notebooks/model_api.ipynb @@ -44,7 +44,7 @@ ")\n", "natural_conversion = NaturalConversion(subject=infected, outcome=immune)\n", "sir_template_model = TemplateModel(templates=[controlled_conversion, natural_conversion])\n", - "sir_template_model_dict = sir_template_model.m()\n", + "sir_template_model_dict = sir_template_model.model_dump()\n", "print(sir_template_model.model_dump_json())" ] }, From 3a0b723831a5f9322bac159960c180d4ea53a495 Mon Sep 17 00:00:00 2001 From: nanglo123 Date: Mon, 23 Sep 2024 15:08:14 -0400 Subject: [PATCH 32/32] Update deprecated copy method --- notebooks/hackathon_2024.02/scenario1/epi_scenario1.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notebooks/hackathon_2024.02/scenario1/epi_scenario1.ipynb b/notebooks/hackathon_2024.02/scenario1/epi_scenario1.ipynb index 9e2513426..7dcd30bd7 100644 --- a/notebooks/hackathon_2024.02/scenario1/epi_scenario1.ipynb +++ b/notebooks/hackathon_2024.02/scenario1/epi_scenario1.ipynb @@ -799,7 +799,7 @@ "# Apply country-specific data to model\n", "for country, country_data in data.items():\n", " # Create a copy of the model for this country\n", - " seird_age_strat_country = seird_base_age_strat.copy(deep=True)\n", + " seird_age_strat_country = seird_base_age_strat.model_copy(deep=True)\n", " \n", " pop_df = country_data['population']\n", " contact_matrix_df = country_data['contact_matrix']\n", @@ -1025,7 +1025,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.10.12" } }, "nbformat": 4,