Skip to content

Commit

Permalink
Merge branch 'local-mvp'
Browse files Browse the repository at this point in the history
  • Loading branch information
jcschaff committed Jan 14, 2025
2 parents 31b235c + dd4a3d4 commit 6164678
Show file tree
Hide file tree
Showing 35 changed files with 5,758 additions and 926 deletions.
115 changes: 100 additions & 15 deletions biosim_server/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@
from temporalio import workflow

from biosim_server.omex_sim.biosim1.models import BiosimSimulatorSpec, SourceOmex
from biosim_server.verify.workflows.runs_verify_workflow import RunsVerifyWorkflowOutput, RunsVerifyWorkflowInput, \
RunsVerifyWorkflow, RunsVerifyWorkflowStatus

with workflow.unsafe.imports_passed_through():
from datetime import datetime, UTC
from datetime import datetime, UTC, timedelta
from pathlib import Path

import dotenv
import uvicorn
from fastapi import FastAPI, File, UploadFile, Query, APIRouter, Depends, HTTPException
from starlette.middleware.cors import CORSMiddleware

from biosim_server.dependencies import get_biosim_service, get_file_service, get_temporal_client, \
from biosim_server.dependencies import get_file_service, get_temporal_client, \
init_standalone, shutdown_standalone
from biosim_server.log_config import setup_logging
from biosim_server.omex_verify.workflows.omex_verify_workflow import OmexVerifyWorkflow, OmexVerifyWorkflowInput, \
from biosim_server.verify.workflows.omex_verify_workflow import OmexVerifyWorkflow, OmexVerifyWorkflowInput, \
OmexVerifyWorkflowOutput, OmexVerifyWorkflowStatus

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -115,16 +117,16 @@ def root() -> dict[str, str]:


@app.post(
"/verify",
"/verify_omex",
response_model=OmexVerifyWorkflowOutput,
name="Uniform Time Course Comparison from OMEX/COMBINE archive",
operation_id="verify",
operation_id="start-verify-omex",
tags=["Verification"],
dependencies=[Depends(get_temporal_client), Depends(get_file_service)],
summary="Compare UTC outputs from a deterministic SBML model within an OMEX/COMBINE archive.")
async def verify(
async def start_verify_omex(
uploaded_file: UploadFile = File(..., description="OMEX/COMBINE archive containing a deterministic SBML model"),
workflow_id_prefix: str = Query(default="verification-", description="Prefix for the workflow id."),
workflow_id_prefix: str = Query(default="omex-verification-", description="Prefix for the workflow id."),
simulators: list[str] = Query(default=["amici", "copasi", "pysces", "tellurium", "vcell"],
description="List of simulators 'name' or 'name:version' to compare."),
include_outputs: bool = Query(default=True,
Expand Down Expand Up @@ -157,7 +159,6 @@ async def verify(
source_omex = SourceOmex(omex_s3_file=s3_path, name="name")
workflow_id = f"{workflow_id_prefix}{uuid.uuid4()}"
omex_verify_workflow_input = OmexVerifyWorkflowInput(
workflow_id=workflow_id,
source_omex=SourceOmex(omex_s3_file=s3_path, name="name"),
user_description=user_description,
requested_simulators=simulator_specs,
Expand All @@ -184,28 +185,112 @@ async def verify(
workflow_input=omex_verify_workflow_input,
workflow_status=OmexVerifyWorkflowStatus.PENDING,
timestamp=str(datetime.now(UTC)),
workflow_id=workflow_id,
workflow_run_id=workflow_handle.run_id
)
return omex_verify_workflow_output


@app.get(
"/get-output/{workflow_id}",
"/verify_omex/{workflow_id}",
response_model=OmexVerifyWorkflowOutput,
operation_id='get-output',
operation_id='get-verify-omex',
tags=["Results"],
dependencies=[Depends(get_biosim_service), Depends(get_file_service)],
dependencies=[Depends(get_temporal_client)],
summary='Get the results of an existing verification run.')
async def get_output(workflow_id: str) -> OmexVerifyWorkflowOutput:
logger.info(f"in get /get-output/{workflow_id}")
async def get_verify_omex(workflow_id: str) -> OmexVerifyWorkflowOutput:
logger.info(f"in get /verify_omex/{workflow_id}")

try:
# query temporal for the workflow output
temporal_client = get_temporal_client()
assert temporal_client is not None
workflow_handle = temporal_client.get_workflow_handle(workflow_id=workflow_id)
workflow_handle = temporal_client.get_workflow_handle(workflow_id=workflow_id,
result_type=OmexVerifyWorkflowOutput)
workflow_output: OmexVerifyWorkflowOutput = await workflow_handle.query("get_output",
result_type=OmexVerifyWorkflowOutput)
result_type=OmexVerifyWorkflowOutput,
rpc_timeout=timedelta(seconds=5))
return workflow_output
except Exception as e2:
exc_message = str(e2)
msg = f"error retrieving verification job output with id: {workflow_id}: {exc_message}"
logger.error(msg, exc_info=e2)
raise HTTPException(status_code=404, detail=msg)


@app.post(
"/verify_runs",
response_model=RunsVerifyWorkflowOutput,
name="Uniform Time Course Comparison from biosimulations runs",
operation_id="start-verify-runs",
tags=["Verification"],
dependencies=[Depends(get_temporal_client)],
summary="Compare UTC outputs from a a list of biosimulation runs.")
async def start_verify_runs(
workflow_id_prefix: str = Query(default="runs-verification-", description="Prefix for the workflow id."),
biosimulations_run_ids: list[str] = Query(description="List of biosimulations run IDs to compare."),
include_outputs: bool = Query(default=True,
description="Whether to include the output data on which the comparison is based."),
user_description: str = Query(..., description="User description of the verification run."),
rel_tol: float = Query(default=1e-6, description="Relative tolerance to use for proximity comparison."),
abs_tol: float = Query(default=1e-9, description="Absolute tolerance to use for proximity comparison."),
observables: Optional[list[str]] = Query(default=None,
description="List of observables to include in the return data.")
) -> RunsVerifyWorkflowOutput:

# ---- create workflow input ---- #
workflow_id = f"{workflow_id_prefix}{uuid.uuid4()}"
runs_verify_workflow_input = RunsVerifyWorkflowInput(
biosimulations_run_ids=biosimulations_run_ids,
user_description=user_description,
include_outputs=include_outputs,
rTol=rel_tol,
aTol=abs_tol,
observables=observables)

# ---- invoke workflow ---- #
logger.info(f"starting verify workflow for biosim run IDs {biosimulations_run_ids}")
temporal_client = get_temporal_client()
assert temporal_client is not None
workflow_handle = await temporal_client.start_workflow(
RunsVerifyWorkflow.run,
args=[runs_verify_workflow_input],
task_queue="verification_tasks",
id=workflow_id,
)
logger.info(f"started workflow with id {workflow_id}")
assert workflow_handle.id == workflow_id

# ---- return initial workflow output ---- #
runs_verify_workflow_output = RunsVerifyWorkflowOutput(
workflow_input=runs_verify_workflow_input,
workflow_status=RunsVerifyWorkflowStatus.PENDING,
timestamp=str(datetime.now(UTC)),
workflow_id=workflow_id,
workflow_run_id=workflow_handle.run_id
)
return runs_verify_workflow_output


@app.get(
"/verify_runs/{workflow_id}",
response_model=RunsVerifyWorkflowOutput,
operation_id='get-verify-runs',
tags=["Results"],
dependencies=[Depends(get_temporal_client)],
summary='Get the results of an existing verification run for biosimulation runs.')
async def get_verify_runs(workflow_id: str) -> RunsVerifyWorkflowOutput:
logger.info(f"in get /verify_runs/{workflow_id}")

try:
# query temporal for the workflow output
temporal_client = get_temporal_client()
assert temporal_client is not None
workflow_handle = temporal_client.get_workflow_handle(workflow_id=workflow_id,
result_type=RunsVerifyWorkflowOutput)
workflow_output: RunsVerifyWorkflowOutput = await workflow_handle.query("get_output",
result_type=RunsVerifyWorkflowOutput,
rpc_timeout=timedelta(seconds=5))
return workflow_output
except Exception as e2:
exc_message = str(e2)
Expand Down
5 changes: 2 additions & 3 deletions biosim_server/io/file_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass

from pydantic import BaseModel
from temporalio import workflow

with workflow.unsafe.imports_passed_through():
Expand All @@ -10,8 +10,7 @@
import aiofiles
import hashlib

@dataclass
class ListingItem:
class ListingItem(BaseModel):
Key: str
LastModified: datetime
ETag: str
Expand Down
12 changes: 5 additions & 7 deletions biosim_server/omex_sim/biosim1/biosim_service_rest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import json
import logging
import os
from dataclasses import asdict
from pathlib import Path
from typing import AsyncGenerator

Expand Down Expand Up @@ -32,8 +30,8 @@ async def get_sim_run(self, simulation_run_id: str) -> BiosimSimulationRun:
id=res["id"],
name=res["name"],
simulator=res['simulator'],
simulator_version=res['simulatorVersion'],
simulator_digest=res['simulatorDigest'],
simulatorVersion=res['simulatorVersion'],
simulatorDigest=res['simulatorDigest'],
status=BiosimSimulationRunStatus(res['status'])
)

Expand All @@ -59,7 +57,7 @@ async def run_biosim_sim(self, local_omex_path: str, omex_name: str,
with Path(local_omex_path).open('rb') as f:
data = FormData()
data.add_field(name='file', value=f, filename='omex.omex', content_type='multipart/form-data')
data.add_field(name='simulationRun', value=json.dumps(asdict(simulation_run_request)),
data.add_field(name='simulationRun', value=simulation_run_request.model_dump_json(),
content_type='multipart/form-data')

async with session.post(url=api_base_url + '/runs', data=data) as resp:
Expand All @@ -73,8 +71,8 @@ async def run_biosim_sim(self, local_omex_path: str, omex_name: str,
id=res["id"],
name=res["name"],
simulator=res['simulator'],
simulator_version=res['simulatorVersion'],
simulator_digest=res['simulatorDigest'],
simulatorVersion=res['simulatorVersion'],
simulatorDigest=res['simulatorDigest'],
status=BiosimSimulationRunStatus(res['status'])
)

Expand Down
56 changes: 38 additions & 18 deletions biosim_server/omex_sim/biosim1/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from dataclasses import dataclass
from enum import Enum, StrEnum
from enum import StrEnum
from typing import Optional

from pydantic import BaseModel
Expand All @@ -17,6 +16,14 @@ class HDF5Dataset(BaseModel):
shape: list[int]
attributes: list[HDF5Attribute]

@property
def sedml_labels(self) -> list[str]:
for attr in self.attributes:
if attr.key == "sedmlDataSetLabels":
if isinstance(attr.value, list) and all(isinstance(v, str) for v in attr.value):
return attr.value # type: ignore
return []


class HDF5Group(BaseModel):
name: str
Expand All @@ -30,9 +37,16 @@ class HDF5File(BaseModel):
uri: str
groups: list[HDF5Group]

@property
def datasets(self) -> dict[str, HDF5Dataset]:
dataset_dict: dict[str, HDF5Dataset] = {}
for group in self.groups:
for dataset in group.datasets:
dataset_dict[dataset.name] = dataset
return dataset_dict


@dataclass
class Hdf5DataValues:
class Hdf5DataValues(BaseModel):
# simulation_run_id: str
# dataset_name: str
shape: list[int]
Expand All @@ -50,20 +64,17 @@ class BiosimSimulationRunStatus(StrEnum):
UNKNOWN = 'UNKNOWN'


@dataclass
class SourceOmex:
class SourceOmex(BaseModel):
name: str
omex_s3_file: str


@dataclass
class BiosimSimulatorSpec:
class BiosimSimulatorSpec(BaseModel):
simulator: str
version: Optional[str] = None


@dataclass
class BiosimSimulationRunApiRequest:
class BiosimSimulationRunApiRequest(BaseModel):
name: str # what does this correspond to?
simulator: str
simulatorVersion: str
Expand All @@ -73,18 +84,27 @@ class BiosimSimulationRunApiRequest:
# memory: Optional[int] = None (in GB)


@dataclass
class BiosimSimulationRun:
class BiosimSimulationRun(BaseModel):
id: str
name: str
simulator: str
simulator_version: str
simulator_digest: str
simulatorVersion: str
status: BiosimSimulationRunStatus


@dataclass
class SimulatorComparison:
simulatorDigest: Optional[str] = None
cpus: Optional[int] = None
memory: Optional[int] = None # (in GB)
maxTime: Optional[int] = None # (in minutes)
envVars: Optional[list[str]] = None # list of environment variables (e.g., ["OMP_NUM_THREADS=4"])
purpose: Optional[str] = None # what does this correspond to?
submitted: Optional[str] = None # datetime string (e.g. 2025-01-10T19:51:11.424Z)
updated: Optional[str] = None # datetime string (e.g. 2025-01-10T19:51:11.424Z)
projectSize: Optional[int] = None # (in bytes)
resultsSize: Optional[int] = None # (in bytes)
runtime: Optional[int] = None # (in milliseconds)
email: Optional[str] = None


class SimulatorComparison(BaseModel):
simRun1: BiosimSimulationRun
simRun2: BiosimSimulationRun
equivalent: bool
Loading

0 comments on commit 6164678

Please sign in to comment.