Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Copasi javascript support #2015

Merged
merged 36 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a166dd5
Add initial support for interpreting COPASI models using `COPASI.js` …
whomingbird Jun 18, 2024
ced0bde
move 'plotly-2.27.0.min.js' to 'application.js'
whomingbird Jun 18, 2024
14b9533
Hide "Simulate Model on Copasi" button if no local model file exists
whomingbird Jun 20, 2024
a2c309c
Load the first COPASI-compatible item when multiple items are associa…
whomingbird Jun 20, 2024
ace41da
bug fix: Load the first COPASI-compatible item to CopasiUI (the desk …
whomingbird Jun 21, 2024
a37cac0
make sure a non-public model can be loaded to Copasi with a temporary…
whomingbird Jun 21, 2024
4b84753
Show the "Simulate Model on Copasi" button on the model page if the u…
whomingbird Jun 21, 2024
32ff6ad
When the model is not publicly accessible but can be downloaded by th…
whomingbird Jun 27, 2024
79da9d3
minor tweak
whomingbird Jun 28, 2024
8715db6
add tests for 'Simulate Model on Copasi button visibility'
whomingbird Jun 28, 2024
1fc1190
delete unnecessary HTML
whomingbird Jun 28, 2024
950b9f6
add unit tests for copasi models
whomingbird Jun 28, 2024
e301b2f
add tests for "copasi simulate" action
whomingbird Jun 28, 2024
fe690c0
update route
whomingbird Jun 28, 2024
6fe1807
add test to verify the correct version is loaded in the simulation
whomingbird Jul 1, 2024
44bc70f
introduces three input fields for parameter inputs on the COPASI simu…
whomingbird Oct 2, 2024
3515cbb
add tabs for showing details of cps model
whomingbird Oct 2, 2024
ebc43ec
refactoring copasi javascript
whomingbird Oct 9, 2024
b386d96
update the view and fix the tab display problem
whomingbird Oct 10, 2024
38d1e97
Check the functionality of the 'Simulate in CopasiUI' button for mode…
whomingbird Oct 10, 2024
b5f30ff
Ensure `copasi_enabled` is checked before executing the `copasi_simul…
whomingbird Oct 10, 2024
d83cd29
add a test of simulating copasi when model is private
whomingbird Oct 10, 2024
5daeced
Merge branch 'main' into copasi-javascript-support
whomingbird Oct 10, 2024
4a3c590
Log run actions for copasi simulation
whomingbird Oct 10, 2024
1bfb2f7
fix XSS vulnerability issue
whomingbird Oct 11, 2024
de276bf
move the 3rd party javascript to vendor/assets/javascript
whomingbird Oct 16, 2024
5b849fa
fix typo
whomingbird Oct 16, 2024
f7840b6
no migration, so no changes in schema.rb
whomingbird Oct 16, 2024
c8076ab
don't use :x=> !
whomingbird Oct 16, 2024
31c7c76
update tooltip text of copasi button
whomingbird Oct 16, 2024
36d8123
merge main branch
whomingbird Oct 16, 2024
665d9d6
refactoring
whomingbird Oct 16, 2024
b37f8cd
correct the action translation
whomingbird Oct 16, 2024
edad931
fix test
whomingbird Oct 17, 2024
4348589
use `with_config_value` in the test
whomingbird Oct 17, 2024
cf74eeb
merge main
whomingbird Oct 18, 2024
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
5 changes: 5 additions & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@
//= require select2.full.min
//= require licenses
//= require svg-pan-zoom-3.6.1/svg-pan-zoom.min
//= require copasi/copasi
//= require copasi/copasijs
//= require copasi/copasi_simulation
//= require plotly-2.27.0.min

79 changes: 79 additions & 0 deletions app/assets/javascripts/copasi/copasi_simulation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// app/assets/javascripts/copasi_simulation.js

var copasi = null;

function automaticChanged() {
var autoStepSize = document.getElementById("autoStepSize").checked;
document.getElementById("numPoints").disabled = autoStepSize;
}

function loadIntoCOPASI() {
var info = copasi.loadModel(document.getElementById("cps").value);
document.getElementById("model_name").innerHTML = "none";
if (info['status'] != "success") {
document.getElementById("simulation_error").innerHTML = "Error loading model: " + info['messages'];
document.getElementById('simulation_error').hidden = false;
}
document.getElementById("model_name").innerHTML = 'Model name: ' + info['model']['name'];
document.getElementById("copasi_version").innerHTML = 'Copasi version: ' + copasi.version;
document.getElementById('simulation_info').hidden = false;
}

function simulate() {
if (copasi == null) {
alert('There is a problem to load Copasi simulator.');
return;
}
loadIntoCOPASI();
runSimulation();
}

function runSimulation() {
var autoStepSize = document.getElementById("autoStepSize").checked;
var timeStart = parseFloat(document.getElementById("startTime").value);
var timeEnd = parseFloat(document.getElementById("endTime").value);

if (autoStepSize) {
var result = copasi.simulateYaml({
"problem": {
"AutomaticStepSize": true,
"Duration": timeEnd,
"OutputStartTime": timeStart
}
});
loadPlotFromResult(result);
return;
}

var numPoints = parseInt(document.getElementById("numPoints").value);
var result = copasi.simulateEx(timeStart, timeEnd, numPoints);
loadPlotFromResult(result);
}

function loadPlotFromResult(result) {
if (typeof result === 'string') {
result = JSON.parse(result);
}

clearResults();
document.getElementById("data").innerHTML = JSON.stringify(result);

var data = [];
for (var i = 1; i < result.num_variables; i++) {
data.push({
name: result.columns[i][0],
x: result.columns[0],
y: result.columns[i],
type: "scatter",
name: result.titles[i]
});
}

Plotly.newPlot('chart', data);
}

function clearResults() {
document.getElementById("data").innerHTML = "";
document.getElementById("chart").innerHTML = "";
}

1 change: 1 addition & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ def log_event
action = 'inline_view' if action == 'explore'
action = 'download' if action == 'ro_crate'
action = 'run' if action == 'simulate'
action = 'run' if action == 'copasi_simulate'
if %w(show create update destroy download inline_view run).include?(action)
check_log_exists(action, controller_name, object)
ActivityLog.create(action: action,
Expand Down
1 change: 1 addition & 0 deletions app/controllers/models_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ModelsController < ApplicationController
before_action :find_other_version, :only => [:compare_versions]

include Seek::Jws::Simulator
include Seek::Copasi::Simulator
include Seek::Publishing::PublishingCommon

include Bives
Expand Down
21 changes: 16 additions & 5 deletions app/helpers/assets_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,26 @@ def download_or_link_button(asset, download_path, link_url, _human_name = nil, o
end
end

def open_with_copasi_button (asset)
def open_with_copasi_js_button

tooltip_text = "Simulate model in the browser with javascript library"
button_link_to 'Simulate Online', 'copasi', '#', class: 'btn btn-primary btn-block', onclick: 'simulate()', disabled: @blob.nil?, 'data-tooltip' => tooltip(tooltip_text)

end

def open_with_copasi_ui_button

blob = @display_model.copasi_supported_content_blobs.first

auth_code = @model.special_auth_codes.where('code LIKE ?', 'copasi_%').first.code unless @model.can_download?(nil)

download_path = polymorphic_path([@model, blob], action: :download, code: auth_code)

files = asset.content_blobs
download_path = polymorphic_path([files.first.asset, files.first], action: :download, code: params[:code])
copasi_download_path = "copasi://process?downloadUrl=http://"+request.host_with_port+download_path+"&activate=Time%20Course&createPlot=Concentrations%2C%20Volumes%2C%20and%20Global%20Quantity%20Values&runTask=Time-Course"

tooltip_text_copasi_button = "Simulate the publicly accessible model in your local installed Copasi. "
tooltip_text_copasi_button = "Simulate your model locally using desk application CopasiUI."

button= button_link_to('Simulate Model in Copasi', 'copasi', copasi_download_path, class: 'btn btn-default', disabled: asset.download_disabled?, 'data-tooltip' => tooltip(tooltip_text_copasi_button))
button= button_link_to('Simulate in CopasiUI', 'copasi', copasi_download_path, class: 'btn btn-primary btn-block', disabled: @blob.nil?, 'data-tooltip' => tooltip(tooltip_text_copasi_button))

button
end
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/models_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ def jws_online_logo
end

def show_copasi_button?
Seek::Config.copasi_enabled && (@display_model.policy.access_type == Policy::ALL_USERS) && @display_model.is_copasi_supported?
Seek::Config.copasi_enabled && @display_model.is_copasi_supported? && @display_model.is_downloadable? && @display_model.can_download?(current_user)
Copy link
Member

Choose a reason for hiding this comment

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

this duplicates and could reuse @display_model.can_run_copasi?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

can_run_copasi? is defined for the "model" model, can not be called by @display_model ( versioning model)

end
end
8 changes: 8 additions & 0 deletions app/models/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,17 @@ def model_format
end

def can_run?
can_run_jws? || can_run_copasi?
end

def can_run_jws?
Seek::Config.jws_enabled && can_download? && is_jws_supported?
end

def can_run_copasi?
Seek::Config.copasi_enabled && can_download? && is_copasi_supported?
end

private

def check_for_sbml_format
Expand Down
10 changes: 5 additions & 5 deletions app/views/models/_buttons.html.erb
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<% if show_copasi_button? %>
<%= open_with_copasi_button @display_model %>
<%= button_link_to("Simulate #{t('model')} on Copasi", 'copasi', copasi_simulate_model_path(version: @display_model.version, code: params[:code]),method: :get) %>
<% end %>

<% if show_jws_simulate? %>
<%= button_link_to("Simulate #{t('model')} on JWS", 'execute',
simulate_model_path(item, :version => @display_model.version, :code => params[:code]), :method=>:post,:title => "Simulate #{t('model')} on JWS") %>
simulate_model_path(item, version: @display_model.version, code: params[:code]), method: :post, title: "Simulate #{t('model')} on JWS") %>
<% end %>

<% if Seek::Config.sycamore_enabled && @display_model.contains_sbml? %>
<% excutable_content_blob = @display_model.content_blobs.detect{|cb| cb.is_sbml?}
if excutable_content_blob.is_in_simulatable_size_limit? && can_download_asset?(@model, params[:code]) -%>
<%= form_tag("http://sycamore.eml.org/sycamore/submission.jsp", :id => 'sycamore-form', :target => '_blank') do -%>
<%= form_tag("http://sycamore.eml.org/sycamore/submission.jsp", id: 'sycamore-form', target: '_blank') do -%>
<%= hidden_field_tag 'sbml_model' -%>
<%= hidden_field_tag 'sender', 'seek' -%>
<%= button_link_to "Simulate #{t('model')} on Sycamore", 'execute', url_for(:action => 'submit_to_sycamore', :id => @model.id, :version => @display_model.version), :remote => true, :method => :post -%>
<%= button_link_to "Simulate #{t('model')} on Sycamore", 'execute', url_for(action: 'submit_to_sycamore', id: @model.id, version: @display_model.version), remote: true, method: :post -%>
<% end -%>
<% end -%>
<% end %>

<%= render :partial => 'assets/asset_buttons', :locals => {:asset => item, :version => version} -%>
<%= render partial: 'assets/asset_buttons', locals: {asset: item, version: version} -%>
105 changes: 105 additions & 0 deletions app/views/models/copasi_simulate.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<% content_for(:buttons) { button_link_to("Back to #{t('model')}", 'back', model_path(version: @display_model.version, code: params[:code])) } %>
<%= render partial: "general/item_title",locals: {item: @model, title_postfix: " - Copasi #{t('model')} Simulation"} %>

<div class="container-fluid">
<div id="simulation_error" class="alert alert-danger" role="alert" hidden=""></div>
<div id="simulation_info" class="alert alert-info" role="alert" hidden="">
<div id="model_name"></div>
<div id="copasi_version"></div>
</div>
<div id="chart"></div>
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="col-sm-2">
<%= label_tag :start_time, "Start Time" %>
<%= number_field_tag :start_time, 0, class: "form-control", placeholder: "Enter start time", id: "startTime" %>
</div>
<div class="col-sm-2">
<%= label_tag :end_time, "End Time:" %>
<%= number_field_tag :end_time, 10, class: "form-control", placeholder: "Enter end time", id: "endTime" %>
</div>
<div class="col-sm-2">
<%= label_tag :num_points, "Number of Points:" %>
<%= number_field_tag :num_points, 101, class: "form-control", placeholder: "Enter number of points", id: "numPoints" %>
</div>
<div class="col-sm-3">
<%= label_tag :simulate_js, "&nbsp;".html_safe %>
<%= open_with_copasi_js_button %>
</div>
<div class="col-sm-3">
<label for="simulate_ui"> &nbsp;</label>
<%= open_with_copasi_ui_button %>
</div>
</div>
<div class="row">
<div class="col-sm-3">
<%= label_tag :auto_step_size, "Automatic Step Size:" %>
<%= check_box_tag :auto_step_size, 1, false, onclick: "automaticChanged()", id: "autoStepSize" %>
</div>
</div>
</div>
</div>
<div>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item active in">
<a class="nav-link" data-toggle="tab" href="#cps_form" role="tab" aria-controls="cps_form" aria-selected="true">CPS Model</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#yaml_form" role="tab" aria-controls="yaml_form" aria-selected="false">YAML</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#data_form" role="tab" aria-controls="data_form" aria-selected="false">Data</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade active in" id="cps_form" role="tabpanel">
<div class="form-group">
<%= label_tag :cps, "CPS Model:" %>
<%= text_area_tag :cps, nil, class: "form-control", id: "cps", rows: 150 %>
</div>
</div>
<div class="tab-pane fade" id="yaml_form" role="tabpanel">
<div class="form-group">
<%= label_tag :yaml, "Processing: " %>
<small>
Changes for processing in yaml for the problem:
<pre>{"problem":{"Duration": 100, "StepNumber": 100, "StepSize": 0.1}}</pre> or
<pre>{"method": {"name": "Stochastic (Gibson + Bruck)"}}</pre> or for changes of
initial values in the form of display names. So:
<ul>
<li>
<pre>[A]_0</pre> for initial concentration of species A
</li>
<li>
<pre>Values[t].InitialValue</pre> for initial value of parameter t
</li>
<li>
<pre>(r1).k</pre> for the value of k of reaction r1.
</li>
</ul>
<pre>{"initial_values": {"[A]_0": 10.0, "Values[t].InitialValue": 0.1, "(r1).k": 0.1}}</pre>
</small>
<%= text_area_tag :yaml, "{}", class: "form-control", id: "yaml", rows: 8 %>
</div>
</div>
<div class="tab-pane fade" id="data_form" role="tabpanel">
<div class="form-group">
<%= label_tag :data, "Data Yaml:" %>
<%= text_area_tag :data, nil, class: "form-control", id: "data", rows: 8 %>
</div>
</div>
</div>
</div>
</div>

<script>
$j(document).ready(function () {
createCpsModule().then(module => {
copasi = new COPASI(module);
console.log(copasi.version);
});

$j('#cps').val("<%= escape_javascript @blob.html_safe %>");
});
</script>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@
post :execute
get :simulate
post :simulate
get :copasi_simulate
end
resources :model_images, only: [:show]
resources :people, :programmes, :projects, :investigations, :assays, :studies, :publications, :events, :collections, :organisms, :human_diseases, only: [:index]
Expand Down
92 changes: 92 additions & 0 deletions lib/seek/copasi/simulator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
module Seek
module Copasi
module Simulator
extend ActiveSupport::Concern
include ERB::Util
included do
before_action :find_model, :find_display_asset_for_copasi, :select_model_file_for_simulation, only: [:copasi_simulate]
before_action :fetch_special_auth_code, if: -> { is_special_auth_code_required? }, only: [:copasi_simulate]
before_action :copasi_enabled, only: [:copasi_simulate]
end

def copasi_simulate
render 'copasi_simulate'
end

def find_model
@model = Model.find_by_id(params[:id])
end

# # If the content blob is not available locally, fetch a copy from the remote URL
def select_model_file_for_simulation

content_blob = select_copasi_content_blob

if content_blob.nil?
flash.now[:error] = 'The selected version does not contain a format supported by COPASI.'
else
if content_blob.file_exists?
@blob = (File.read(content_blob.file))
else
blob_url = content_blob.url
begin
handler = ContentBlob.remote_content_handler_for(blob_url)
data = handler.fetch
@blob = (File.read(data))
true
rescue Seek::DownloadHandling::BadResponseCodeException => e
flash.now[:error] = "URL could not be accessed: #{e.message}"
false
rescue StandardError => e
flash.now[:error] = 'There is a problem to load the model file.'
false
end
end
end
end

# select the first COPASI-compatible content_blob when multiple items are associated with the display model.
def select_copasi_content_blob
blob = @display_model.copasi_supported_content_blobs.first
blob
end

def find_display_asset_for_copasi
find_display_asset
end

def copasi_enabled
unless Seek::Config.copasi_enabled
respond_to do |format|
flash[:error] = "Interaction with Copasi Online is currently disabled"
format.html { redirect_to model_path(@model, :version => @display_model.version) }
end
return false
end
end

private

def special_auth_codes_with_copasi_prefix
@model.special_auth_codes.where('code LIKE ?', 'copasi_%')
end

def is_special_auth_code_required?
# If the model is not publicly accessible but can be downloaded by the current user, the special auth code will be required.
Seek::Config.copasi_enabled && @display_model.is_copasi_supported? && @model.can_download?(current_user) && [email protected]_download?(nil)
end

# fetches or generates a special auth code with a "copasi_" prefix, which is used by COPASI desk application to load the non public model
def fetch_special_auth_code
copasi_codes = special_auth_codes_with_copasi_prefix
return copasi_codes.first unless copasi_codes.empty?

auth_code = SpecialAuthCode.create(expiration_date: Time.now + 1.day, code: "copasi_#{SecureRandom.hex(10)}")
@model.special_auth_codes << auth_code
auth_code
end

end
end
end

Loading