Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ JuliaLocalPreferences.toml

# icloud/finder specific files on macOS.
*.DS_Store

# Electron app
GUI/electron/node_modules/
GUI/electron/dist/

# Julia compiled app output (large binary, built locally)
GUI/backend/julia-app/

# Julia build environment manifest (resolved locally)
GUI/backend/build/Manifest.toml
1 change: 0 additions & 1 deletion GUI/backend/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ authors = ["Helmut Strey <Helmut.Strey@stonybrook.edu>"]

[deps]
Catalyst = "479239e8-5488-4da2-87a7-35f2df7eef83"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a"
Expand Down
40 changes: 40 additions & 0 deletions GUI/backend/build.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
Build script for NT Cycling Model – compiles the Julia backend into a
standalone executable using PackageCompiler.jl.

Usage (from repo root):
julia --project=GUI/backend/build GUI/backend/build.jl

Prerequisites:
1. Julia 1.10+ installed and on PATH.
2. Backend dependencies resolved:
julia --project=GUI/backend -e 'using Pkg; Pkg.instantiate()'
3. PackageCompiler installed in the build env:
julia --project=GUI/backend/build -e 'using Pkg; Pkg.instantiate()'

Output: GUI/backend/julia-app/
bin/server – compiled executable
lib/ – Julia runtime + precompiled sysimage
share/ – Julia standard library sources

The julia-app/ directory is then bundled by electron-builder as an
extraResource inside the Electron app (see GUI/electron/package.json).
"""

using PackageCompiler

backend_dir = joinpath(@__DIR__) # GUI/backend/
output_dir = joinpath(@__DIR__, "julia-app")
precompile = joinpath(@__DIR__, "precompile_app.jl")

@info "Building NT Cycling app..." backend_dir output_dir

create_app(
backend_dir,
output_dir;
precompile_execution_file = precompile,
force = true,
include_lazy_artifacts = true,
)

@info "Build complete." output_dir
2 changes: 2 additions & 0 deletions GUI/backend/build/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[deps]
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
56 changes: 56 additions & 0 deletions GUI/backend/precompile_app.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Precompile workload for PackageCompiler.
# This script exercises key code paths so the compiled sysimage covers them,
# resulting in faster startup and response times.

using Catalyst
using HTTP
using Oxygen
using OrdinaryDiffEqTsit5
using JSON3
using XLSX

include(joinpath(@__DIR__, "src", "NTModelModule.jl"))
using .NTModelModule

# Exercise model construction and simulation
model = NTModel()
sol = simulate(model; tspan=(0.0, 600.0))

# Exercise result extraction (both absolute and percentage modes)
results = extract_results(sol, ["GLU_e", "GABA_i", "GLN_a", "GLN_e", "GLU_i", "GABA_v", "GLN_i", "GLU_a", "KET_b"], false)
results_pct = extract_results(sol, ["GLU_e", "GABA_i"], true)

# Exercise defaults serialisation
defaults = get_defaults(model)

# Exercise JSON round-trip
json_str = JSON3.write(results)
JSON3.read(json_str)
json_str2 = JSON3.write(defaults)
JSON3.read(json_str2)

# Exercise a second simulation with modified parameters (mirrors /api/simulate usage)
model2 = NTModel()
model2.ps[:KET_p] = 3.9
sol2 = simulate(model2; tspan=(0.0, 90.0))
extract_results(sol2, ["GLU_e", "GABA_i"], true)

# Exercise Oxygen route registration — this is the critical step that ensures
# Oxygen's router, @get/@post macro expansions, and handler dispatch are all
# compiled into the sysimage. (serve() itself cannot be called here because
# it blocks, but registering routes + invoking handlers covers all hot paths.)
using NTCycling
NTCycling.register_routes!()

# Exercise handler functions directly to precompile the full request/response
# dispatch path for each endpoint.
NTCycling._handle_defaults(HTTP.Request("GET", "/api/defaults"))

simulate_body = """{"simLength":60,"selectedStates":["GLU_e","GABA_i"],"percentageChange":false}"""
NTCycling._handle_simulate(
HTTP.Request("POST", "/api/simulate",
["Content-Type" => "application/json"],
simulate_body)
)

println("Precompile workload complete")
17 changes: 15 additions & 2 deletions GUI/backend/src/NTCycling.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
module NTCycling
println("Hello")
# does nothing

include("server.jl")

Base.@ccallable function julia_main()::Cint
try
port = parse(Int, get(ENV, "NT_PORT", "8090"))
host = get(ENV, "NT_HOST", "127.0.0.1")
start_server(; host=host, port=port)
catch e
println(stderr, "Error starting server: ", e)
return 1
end
return 0
end

end
41 changes: 20 additions & 21 deletions GUI/backend/src/NTModelModule.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ module NTModelModule

using Catalyst
using OrdinaryDiffEqTsit5
using DataFrames

export NTModel, simulate, get_timeseries, get_defaults, extract_results
export NTModel, simulate, get_defaults, extract_results
export unknowns

mutable struct NTModel
Expand Down Expand Up @@ -262,24 +261,24 @@ function extract_results(sol, selected_states::Vector{String}, percentage_change
)
end

"""
get_timeseries(sol; selected_states=["GLN_e(t)"], percentage_change=false)

Returns time series as DataFrame for selected states.
"""
function get_timeseries(sol; selected_states=["GLN_e(t)"], percentage_change=false)
timeseries = []
for state in selected_states
state_symbol = Symbol(replace(state, "(t)" => ""))
if percentage_change
push!(timeseries, round.(100 .* sol[state_symbol]./(sol[state_symbol][1]) .- 100, digits=1))
else
push!(timeseries, sol[state_symbol])
end
end

df = DataFrame([sol.t, timeseries...], Symbol.(["time", selected_states...]))
return df
end
#"""
# get_timeseries(sol; selected_states=["GLN_e(t)"], percentage_change=false)
#
#Returns time series as DataFrame for selected states.
#"""
#function get_timeseries(sol; selected_states=["GLN_e(t)"], percentage_change=false)
# timeseries = []
# for state in selected_states
# state_symbol = Symbol(replace(state, "(t)" => ""))
# if percentage_change
# push!(timeseries, round.(100 .* sol[state_symbol]./(sol[state_symbol][1]) .- 100, digits=1))
# else
# push!(timeseries, sol[state_symbol])
# end
# end
#
# df = DataFrame([sol.t, timeseries...], Symbol.(["time", selected_states...]))
# return df
#end

end
6 changes: 5 additions & 1 deletion GUI/backend/src/api/defaults.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# GET /api/defaults - Returns default parameters, states, units, descriptions

@get "/api/defaults" function(req::HTTP.Request)
function _handle_defaults(req::HTTP.Request)
model = NTModel()
defaults = get_defaults(model)
return JSON3.write(defaults)
end

function register_defaults_routes!()
@get "/api/defaults" _handle_defaults
end
11 changes: 8 additions & 3 deletions GUI/backend/src/api/examples.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# GET /api/examples - List available examples
# GET /api/examples - List available examples
# GET /api/example/:id - Get specific example configuration

const EXAMPLES = Dict(
Expand Down Expand Up @@ -50,7 +50,7 @@ const EXAMPLES = Dict(
)
)

@get "/api/examples" function(req::HTTP.Request)
function _handle_examples(req::HTTP.Request)
examples_list = [
Dict(
"id" => id,
Expand All @@ -62,10 +62,15 @@ const EXAMPLES = Dict(
return JSON3.write(examples_list)
end

@get "/api/example/{id}" function(req::HTTP.Request, id::String)
function _handle_example_by_id(req::HTTP.Request, id::String)
if haskey(EXAMPLES, id)
return JSON3.write(EXAMPLES[id])
else
return HTTP.Response(404, JSON3.write(Dict("error" => "Example not found")))
end
end

function register_examples_routes!()
@get "/api/examples" _handle_examples
@get "/api/example/{id}" _handle_example_by_id
end
9 changes: 5 additions & 4 deletions GUI/backend/src/api/export.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
# POST /api/export - Generate XLSX file

using XLSX
using DataFrames

@post "/api/export" function(req::HTTP.Request)
function _handle_export(req::HTTP.Request)
body = JSON3.read(String(req.body))

# Create a temporary file for the XLSX
Expand Down Expand Up @@ -97,3 +94,7 @@ using DataFrames
]
return HTTP.Response(200, headers, xlsx_data)
end

function register_export_routes!()
@post "/api/export" _handle_export
end
6 changes: 5 additions & 1 deletion GUI/backend/src/api/simulation.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# POST /api/simulate - Run simulation for both conditions

@post "/api/simulate" function(req::HTTP.Request)
function _handle_simulate(req::HTTP.Request)
body = JSON3.read(String(req.body))

# Create models for each condition
Expand Down Expand Up @@ -59,3 +59,7 @@
"condition2" => results2
))
end

function register_simulation_routes!()
@post "/api/simulate" _handle_simulate
end
89 changes: 51 additions & 38 deletions GUI/backend/src/server.jl
Original file line number Diff line number Diff line change
@@ -1,64 +1,77 @@
using Oxygen
using HTTP
using JSON3
using XLSX

# Include the model
include("NTModelModule.jl")
using .NTModelModule

# Serve static files from public directory
const PUBLIC_DIR = joinpath(@__DIR__, "..", "public")
const PUBLIC_DIR = get(ENV, "NT_PUBLIC_DIR", joinpath(@__DIR__, "..", "public"))

# Main page
@get "/" function(req::HTTP.Request)
html_content = read(joinpath(PUBLIC_DIR, "index.html"), String)
return HTTP.Response(200, ["Content-Type" => "text/html"], html_content)
end
# Include API files at module load time so handler functions are compiled into the sysimage.
# Route *registration* (@get/@post calls) happens inside register_routes!() at runtime,
# because Oxygen's HTTP router state does not survive sysimage serialisation.
include("api/defaults.jl")
include("api/simulation.jl")
include("api/examples.jl")
include("api/export.jl")

function register_routes!()

# Documentation page
@get "/documentation" function(req::HTTP.Request)
filepath = joinpath(PUBLIC_DIR, "docs.html")
if isfile(filepath)
html_content = read(filepath, String)
# Main page
@get "/" function(req::HTTP.Request)
html_content = read(joinpath(PUBLIC_DIR, "index.html"), String)
return HTTP.Response(200, ["Content-Type" => "text/html"], html_content)
else
return HTTP.Response(404, "Documentation page not found")
end
end

# Serve static files (CSS, images, etc.)
@get "/css/{filename}" function(req::HTTP.Request, filename::String)
filepath = joinpath(PUBLIC_DIR, "css", filename)
if isfile(filepath)
content = read(filepath, String)
return HTTP.Response(200, ["Content-Type" => "text/css"], content)
else
return HTTP.Response(404, "File not found")
# Documentation page
@get "/documentation" function(req::HTTP.Request)
filepath = joinpath(PUBLIC_DIR, "docs.html")
if isfile(filepath)
html_content = read(filepath, String)
return HTTP.Response(200, ["Content-Type" => "text/html"], html_content)
else
return HTTP.Response(404, "Documentation page not found")
end
end
end

@get "/images/{filename}" function(req::HTTP.Request, filename::String)
filepath = joinpath(PUBLIC_DIR, "images", filename)
if isfile(filepath)
content = read(filepath)
content_type = endswith(filename, ".png") ? "image/png" : "image/jpeg"
return HTTP.Response(200, ["Content-Type" => content_type], content)
else
return HTTP.Response(404, "File not found")
# Serve static files (CSS, images, etc.)
@get "/css/{filename}" function(req::HTTP.Request, filename::String)
filepath = joinpath(PUBLIC_DIR, "css", filename)
if isfile(filepath)
content = read(filepath, String)
return HTTP.Response(200, ["Content-Type" => "text/css"], content)
else
return HTTP.Response(404, "File not found")
end
end
end

# Include API endpoints
include("api/defaults.jl")
include("api/simulation.jl")
include("api/examples.jl")
include("api/export.jl")
@get "/images/{filename}" function(req::HTTP.Request, filename::String)
filepath = joinpath(PUBLIC_DIR, "images", filename)
if isfile(filepath)
content = read(filepath)
content_type = endswith(filename, ".png") ? "image/png" : "image/jpeg"
return HTTP.Response(200, ["Content-Type" => content_type], content)
else
return HTTP.Response(404, "File not found")
end
end

# API routes
register_defaults_routes!()
register_simulation_routes!()
register_examples_routes!()
register_export_routes!()
end

# Start server
function start_server(; host="127.0.0.1", port::Int=8090)
register_routes!()
println("Starting NT Cycling Model server on http://localhost:$port")
println("Press Ctrl+C to stop the server")
serve(; host=host,port=port)
serve(; host=host, port=port)
end

# Run if executed directly
Expand Down
Loading