Beispiel #1
0
async def index(request: Request):
    """Index frontend web page.
    
    \f
    Parameters
    ----------
    request : Request
        HTTP request, used internally by FastAPI.

    Returns
    -------
    ``fastapi.templating.Jinja2Templates.TemplateResponse``
        Renders greeting template and hyperlinks to simulation rquests and
        results.
    """

    route_simulate = app.url_path_for("frontend_simulate")
    route_results = app.url_path_for("frontend_results")

    return templates.TemplateResponse(
        "index.html", 
        {
            "request": request, 
            "route_simulate": route_simulate,
            "route_results": route_results
        }
    )
Beispiel #2
0
async def results_sim_system_sim_id(request: Request, sim_system: SimSystem,
                                    sim_id: str):
    """Shows graphic results of a simulation in frontend for a given ``sim_id``.
    
    \f
    Parameters
    ----------
    request : Request
        HTTP request, used internally by FastAPI.
    sim_system : SimSystem
        System to be simulated.
    sim_id : str
        ID of the simulation.

    Returns
    -------
    ``fastapi.templating.Jinja2Templates.TemplateResponse``
        Template displaying all the generated plots for a given simulation.
    """

    # Pickle download route
    route_pickle = app.url_path_for("api_download_pickle", sim_id=sim_id)

    # Plot download routes
    plot_paths = app.url_path_for("api_download_plots", sim_id=sim_id)
    if sim_system == SimSystem.HO.value:
        coord = PlotQueryValues_HO.coord.value
        phase = PlotQueryValues_HO.phase.value
        plot_routes = {
            coord: plot_paths + "?value=" + coord,
            phase: plot_paths + "?value=" + phase,
        }
    elif sim_system == SimSystem.ChenLee.value:
        threeD = PlotQueryValues_ChenLee.threeD.value
        project = PlotQueryValues_ChenLee.project.value
        plot_routes = {
            threeD: plot_paths + "?value=" + threeD,
            project: plot_paths + "?value=" + project
        }
    else:
        plot_routes = {}
    
    results_info = {
        "sim_sys": sim_system.value,
        "sim_id": sim_id,
        "route_pickle": route_pickle,
    }

    return templates.TemplateResponse(
        "results-show.html",
        {
            "request": request,
            "plot_routes": plot_routes,
            **results_info
        }
    )
Beispiel #3
0
async def results(request: Request, db: Session = Depends(get_db)):
    """Renders web page showing a list of all the available simulation results.
    
    \f
    Parameters
    ----------
    request : Request
        HTTP request, used internally by FastAPI.
    db : Session
        Database Session, needed to interact with database. This is handled 
        internally.

    Returns
    -------
    ``fastapi.templating.Jinja2Templates.TemplateResponse``
        Template displaying all the available simulation results.
    """
    # Pull all simulations from database
    simulations = crud._get_all_simulations(db)
    simulations = [simulation.__dict__ for simulation in simulations]
    sim_status_url = app.url_path_for("fronted_simulation_status", sim_id="0")
    
    return templates.TemplateResponse(
        "results.html",
        {
            "request": request,
            "simulations": simulations,
            "sim_status_url": sim_status_url,
        }
    )
Beispiel #4
0
async def simulate_id_sim_id(request: Request, sim_id: str):
    """Shows simulation id after asking for simulation in frontend form.

    \f
    Parameters
    ----------
    request : Request
        HTTP request, used internally by FastAPI.
    sim_id : str
        ID of the simulation.

    Returns
    -------
    ``fastapi.templating.Jinja2Templates.TemplateResponse``
        Template displaying the simulation ID and a hyperlink to
        further information about the simulation.
    """
    sim_status_url = app.url_path_for(
        "fronted_simulation_status",
        sim_id=sim_id
    )

    # This same template is used to show simulation id info or simulation
    # status info, here we need simulation id info, so we set status=False.
    return templates.TemplateResponse(
        "simulation-id-or-status.html",
        {
            "request": request,
            "status": False,
            "sim_id": sim_id,
            "sim_status_url": sim_status_url,
        }
    )
Beispiel #5
0
async def simulate_status_sim_id(request: Request, sim_id: str,
                                 db: Session = Depends(get_db)):
    """Shows simulation status for a given simulation via its ``sim_id``.
    
    \f
    Parameters
    ----------
    request : Request
        HTTP request, used internally by FastAPI.
    sim_id : str
        ID of the simulation.
    db : Session
        Database Session, needed to interact with database. This is handled 
        internally.

    Returns
    -------
    ``fastapi.templating.Jinja2Templates.TemplateResponse``
        Template displaying the simulation status and a hyperlinks to
        simulation results in several formats. If simulation id is not
        available (not yet in database), renders a message about the situation.
    """
    try:
        sim_info = crud._get_simulation(db, sim_id)
        username = crud._get_username(db, sim_info.user_id)
    except:
        return templates.TemplateResponse(
            "simulation-id-or-status.html",
            {
                "request": request,
                "sim_id": sim_id,
                "status": True,
                "not_finished": True,
            }
        )

    params = crud._get_parameters(db, sim_id, ParamType.param.value)
    plot_query_values = crud._get_plot_query_values(db, sim_id)

    plots_url = app.url_path_for(
        "api_download_plots",
        sim_id=sim_id
    )
    path_plots = [plots_url + "?value=" + value for value in plot_query_values]

    ini_cndtn = crud._get_parameters(db, sim_id, ParamType.ini_cndtn.value)

    # This same template is used to show simulation id info or simulation
    # status info, here we need simulation status so we set status=True.
    return templates.TemplateResponse(
        "simulation-id-or-status.html",
        {
            "request": request,
            "username": username[0],
            "status": True,
            "ini_cndtn": ini_cndtn,
            "params": params,
            "path_plots": path_plots,
            **sim_info.__dict__
        }
    )
Beispiel #6
0
async def simulate_sim_system_post(request: Request, sim_system: SimSystem,
                                   background_tasks: BackgroundTasks,
                                   db: Session = Depends(get_db),
                                   sim_sys: SimSystem = Form(...),
                                   username: str = Form(...),
                                   t0: float = Form(...),
                                   tf: float = Form(...),
                                   dt: float = Form(...),
                                   t_steps: int = Form(...),
                                   method: IntegrationMethods = Form(...)):
    """Receives the simulation request information from the frontend form and
    requests the simulation to the backend.
    
    \f
    This route receives the form requesting a simulation (filled in
    frontend via GET in route ``/simulate/{sim_system}``). The simulation is
    internally requested using the function 
    :func:`simulation_api.controller.tasks._api_simulation_request`.
    Finally the client is redirected to the "Simulation ID" frontend web
    page, where further information about the simulation is displayed.

    Parameters
    ----------
    request : Request
        HTTP request, used internally by FastAPI.
    sim_system : SimSystem
        System to be simulated.
    background_tasks : BackgroundTasks
        Needed to request simulation to the backend (in background). Handled
        internally by the API.
    db : Session
        Database Session, needed to interact with database. This is handled 
        internally.
    sim_sys : SimSystem
        Form entry: system to be simulated. Must be one of the members of
        SimSystem.
    username : str
        Form entry: name of the user requesting the simulation.
    t0 : float
        Form entry: initial time of simulation.
    tf : float
        Form entry: final time of simulation.
    dt : float
        Form entry: timestep of simulation.
    t_steps:
        Form entry: number of time steps. If different from 0 or ``None``,
        ``dt`` is ignored.
    method : IntegrationMethods
        Form entry: method of integration. Must be a member of
        IntegrationMethods.

    Returns
    -------
    ``fastapi.templating.Jinja2Templates.TemplateResponse`` or ``starlette.responses.RedirectResponse``
        If the client made a mistake filling the form renders the simulation
        request form again. Otherwise, redirects the client to "Simulation ID"
        frontend web page.
    
    Note
    ----
    The values of the form accessed by ``fastapi.Form`` are only
    declared as parameters so that pydantic checks the types, but they are not
    used directly to request the simulation. 
    Here, we access the form directly
    by using the method :meth:`fastapi.Request.form()` as can be seen in the
    first lines of this function. This allows us a better control over the data
    and also to handle different type of forms –which depend on the simulation
    because parameters and initial conditions are intrinsically different for
    different systems.
    """
    # Here we get form directly from request.
    # NOTE The other method is using FastAPI's Form function, but here it is
    # easier to access the form directly from request (as below).
    # NOTE This method does NOT provide pydantic type checking. However, type
    # checking is more or less provided in the frontend by the form itself.
    form = await request.form()
    form = form.__dict__['_dict']

    ########################## Check for some errors ##########################
    error_message = ""
    if not t_steps:
        if not t0 < tf:
            error_message = "in Time Stamp, t0 must be less than tf"
        elif not tf - t0 > dt or dt <= 0:
            error_message = "in Time Stamp, dt must be within 0 and tf - t0"

    # Maximum number of steps: 2e5
    maxsteps = 2e5
    maxsteps_reached_message = "maximum number of time steps is 2e5. " \
                               "Increase dt or decrease t_steps."
    if t_steps and t_steps > maxsteps:
        error_message = maxsteps_reached_message
    elif (tf - t0) / dt > maxsteps:
        error_message = maxsteps_reached_message

    # Check Chen-Lee parameters
    if sim_sys.value == SimSystem.ChenLee.value:
        a, b, c = [float(form[f"param{i}"]) for i in range(3)]
        if not _check_chen_lee_params(a, b, c):
            error_message = "Chen-Lee parameters must satisfy a > 0, and " \
                            "b < 0, and c < 0 and a < -(b + c)"

    if error_message:
        SysSimForm = SimFormDict[sim_system.value]
        sim_form = SysSimForm()
        return templates.TemplateResponse(
            "request-simulation.html",
            {
                "sim_system": str(sim_system.value),
                "request": request,
                "integration_methods": integration_methods,
                "error_message": error_message,
                **sim_form.dict(),
            },
            status_code=400
        )
    ############################## End of check ###############################

    # Change format from form data to SimRequest schema.
    sim_request = _sim_form_to_sim_request(form)
    
    # BUG? Is the simulation request as done in the next lines of code OK?
    # 
    # FIXME
    #  
    # In the first place I was thinking of asking the API to somehow internally
    # go to route "/api/simulate/{sim_system}" BUT I could not do that.
    # I tried using the methods shown below: 
    # 
    # NOTE Method 1) using request.post()
    # request_simulation_url = app.url_path_for(
    #     "api_request_sim",
    #     sim_system=sim_request.system.value
    # )
    # req = requests.post("http://0.0.0.0:5700" + request_simulation_url, data=sim_request.json())
    #
    # NOTE Method 2) using RedirectResponse
    # req = RedirectResponse(request_simulation_url)
    #
    # Neither of the methods worked so I solved it by just doing the same as in
    # route "/api/simulate/{sim_system}" (calling _api_simulation_request as
    # done below)
    
    # Request simulation from backend and get sim_id_response
    sim_id_response = _api_simulation_request(sim_sys, sim_request,
                                              background_tasks, db)
    
    # Redirect client to 'success' page
    # POST/REDIRECT/GET Strategy with 303 status code
    # https://en.wikipedia.org/wiki/Post/Redirect/Get 
    simulation_id_url = app.url_path_for(
        "frontend_simulation_id",
        sim_id=sim_id_response.sim_id
    )
    req = RedirectResponse(simulation_id_url,
                           status_code=HTTP_303_SEE_OTHER)

    return req
Beispiel #7
0
def _api_simulation_request(sim_system: SimSystem, sim_params: SimRequest,
                            background_tasks: BackgroundTasks,
                            db: Session) -> SimIdResponse:
    """Requests simulation to BackgroundTasks.

    Parameters
    ----------
    sim_system : SimSystem
        System to be simulated.
    sim_params : SimRequest
        Contains all the information about the simulation request.
    background_tasks : ``fastapi.BackgroundTasks``
        Object needed to request simulation in the background.
    db : ``sqlalchemy.orm.Session``
        Needed for interaction with database.
    
    Returns
    -------
    sim_id_response : SimIdResponse
        Contains information about simulation request, such as simulation ID
        and others. See
        :class:`~simulation_api.controller.schemas.SimIdResponse` for more
        information.
    """

    ########################## Check for some errors ##########################
    # Check that the simulation parameters are the ones needed for the
    # requested simulation. This is not checked by the pydantic model.
    error_message = ""
    try:
        ParamsModel = SimSystem_to_SimParams[sim_system.value]
        ParamsModel(**sim_params.params)
    except:
        error_message = "Error: you provided the wrong set of parameters. " \
                        "Your simulation was not requested."

    # Check Chen-Lee parameters
    if sim_system.value == SimSystem.ChenLee.value and not error_message:
        params = sim_params.params
        if not _check_chen_lee_params(params["a"], params["b"], params["c"]):
            error_message = "Chen-Lee parameters must satisfy a > 0, and " \
                            "b < 0, and c < 0 and a < -(b + c)"

    if error_message:
        sim_id_response = SimIdResponse(username=sim_params.username,
                                        message=error_message)
        return sim_id_response
    ############################## End of check ###############################

    # Create user in database (meanwhile)
    # FIXME FIXME FIXME
    # In production user can NOT be created here, login will be required.
    user = UserDBSchCreate(username=sim_params.username)
    user = crud._create_user(db, user)
    # Get user_id from user and store it in sim params !
    sim_params.user_id = user.user_id

    # Create an id for the simulation store it in hex notation
    sim_params.sim_id = uuid4().hex

    # Close ccurrent db connection, so that _run_simulation can update table
    db.close()

    # Check that the client is accessing the right path for the right simulation
    # sim_system.value NEEDS to match the request given in JSON as
    # sim_params.system
    if not sim_system.value == sim_params.system.value:
        raise HTTPException(
            status_code=403,
            detail=r"403 - Forbidden : URI's {sim_system} value must coincide "
            r"with 'system' key value in posted JSON file")

    # Simulate system in BACKGROUND
    # TODO TODO TODO Por dentro _run_simulation puede abrir un websocket para
    # TODO TODO TODO indicar que la simulación ya se completó
    background_tasks.add_task(_run_simulation, sim_params)

    # Declare some variables needed as params to SimIdResponse
    sim_status_path = app.url_path_for("api_simulate_status",
                                       sim_id=sim_params.sim_id)

    sim_pickle_path = app.url_path_for("api_download_pickle",
                                       sim_id=sim_params.sim_id)

    message1 = "(When –and if– available) request via GET your simulation's" \
               "status in route 'sim_status_path' or download your results" \
               "(pickle fomat) via GET in route 'sim_pickle_path'"
    message2 = na_message
    message = message1 if sim_params.system in SimSystem else message2

    sim_id_response = SimIdResponse(sim_id=sim_params.sim_id,
                                    user_id=sim_params.user_id,
                                    username=sim_params.username,
                                    sim_sys=sim_params.system,
                                    sim_status_path=sim_status_path,
                                    sim_pickle_path=sim_pickle_path,
                                    message=message)

    return sim_id_response
Beispiel #8
0
def _run_simulation(sim_params: SimRequest) -> None:
    """Runs the requested simulation and stores the outcome in a database.

    This function runs the simulation, stores the simulation parameters in a
    database, stores the simulation result in a pickle and creates and saves
    relevant plots of the simulation.

    Parameters
    ----------
    sim_params : SimRequest
        Contains all the information needed for the simulation.

    Returns
    -------
    None
    """
    # Start session in dbase
    db = SessionLocal()

    # If t_steps is provided in sim_params, generate t_eval
    if sim_params.t_steps:
        sim_params.t_eval = linspace(sim_params.t_span[0],
                                     sim_params.t_span[1], sim_params.t_steps)

    # Convert the SimRequest instance to dict
    sim_params = sim_params.dict()

    # Pop some values Simulation __init__ method does not accept.
    # Remember "system" will be a member of SimSystem defined in simulation_api.controll.schemas
    system = sim_params.pop("system")
    sim_id = sim_params.pop("sim_id")
    user_id = sim_params.pop("user_id")
    sim_params.pop("t_steps")
    sim_params.pop("username")

    basic_info = {
        "sim_id": sim_id,
        "user_id": user_id,
        "date": str(datetime.utcnow()),
        "system": system.value,
    }

    # Check that "system" is in available simulation systems
    # NOTE maybe this is not necessary since pydantic already checks this. In
    # schema request SimRequest requires "system" to be a member of SimSystem.
    if not system in SimSystem:
        create_simulation_status_db = SimulationDBSchCreate(success=False,
                                                            message=na_message,
                                                            **basic_info)
        # Save simulation status in database
        crud._create_simulation(db, create_simulation_status_db)
        # Close db session
        db.close()
        return

    # Try to simulate the system. If there is an exception in simulation store
    # it in database and exit this function
    try:
        # Run simulation and get results as returned by scipy.integrate.solve_ivp
        LocalSimulation = Simulations[system.value]
        simulation_instance = LocalSimulation(**sim_params)
        simulation = simulation_instance.simulate()
    except Exception as e:
        create_simulation_status_db = SimulationDBSchCreate(
            success=False,
            message="Internal Simulation Error: " + str(e),
            **basic_info)
        crud._create_simulation(db, create_simulation_status_db)
        db.close()

        # FIXME FIXME FIXME is it better to raise an exception at this point?
        return

    # Store simulation result in pickle
    _pickle(sim_id + ".pickle", PATH_PICKLES, dict(simulation))

    # Create and save plots
    plot_query_values = _plot_solution(SimResults(sim_results=simulation),
                                       system, sim_id)

    # Save simulation status in database
    create_simulation_status_db = SimulationDBSchCreate(
        method=sim_params["method"],
        route_pickle=app.url_path_for("api_download_pickle", sim_id=sim_id),
        route_results=app.url_path_for("api_simulate_status", sim_id=sim_id),
        route_plots=app.url_path_for("api_download_plots", sim_id=sim_id),
        success=True,
        message=sim_status_finished_message,
        **basic_info)
    crud._create_simulation(db, create_simulation_status_db)

    #  Save plot query values to database
    plot_query_values = [
        PlotDBSchCreate(sim_id=sim_id, plot_query_value=plot_qb)
        for plot_qb in plot_query_values
    ]
    crud._create_plot_query_values(db, plot_query_values)

    # Store simulation parameters in database
    parameters = []
    for key, value in simulation_instance.params.items():
        parameter = ParameterDBSchCreate(sim_id=sim_id,
                                         param_type=ParamType.param.value,
                                         param_key=key,
                                         value=value)
        parameters.append(parameter)
    for i, ini_cndtn_val in enumerate(simulation_instance.ini_cndtn):
        ini_cndtn = ParameterDBSchCreate(sim_id=sim_id,
                                         param_type=ParamType.ini_cndtn,
                                         ini_cndtn_id=i,
                                         value=ini_cndtn_val)
        parameters.append(ini_cndtn)
    crud._create_parameters(db, parameters)

    # Close db session
    db.close()

    return