Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4eb1c0f
Updated default inverse tooltip
Feb 3, 2026
e56a108
Merge branch 'learningFixes' of github.com:MEDomics-UdeS/MEDomicsLab …
Feb 3, 2026
88d952b
Merge branch 'learningFixes' of github.com:MEDomics-UdeS/MEDomicsLab …
Feb 6, 2026
ef2a5f6
Merge branch 'learningFixes' of github.com:MEDomics-UdeS/MEDomicsLab …
Feb 9, 2026
4a1e609
fixed issue in default params in train model node
MahdiAll99 Mar 4, 2026
0c24eef
requests path bug fixed
MahdiAll99 Mar 4, 2026
fae4ae3
very minor bug fix
MahdiAll99 Mar 4, 2026
db6c0ea
Minor css improvement
MahdiAll99 Mar 4, 2026
94bfa28
more css fixes
MahdiAll99 Mar 5, 2026
b9bbb1b
Code generation fixes
MahdiAll99 Mar 5, 2026
aca26f1
Model finalization issue fixed when optim thresh is activated
MahdiAll99 Mar 5, 2026
cee4ab2
Pycaret upgrade
MahdiAll99 Mar 5, 2026
3c0f2a4
Merge branch 'learningFixes' of github.com:MEDomics-UdeS/MEDomicsLab …
Mar 5, 2026
6492b14
Merge branch 'develop' into learningFixes
MahdiAll99 Mar 5, 2026
b5218b0
Merge pull request #546 from MEDomicsLab/learningFixes
MahdiAll99 Mar 5, 2026
33e4bdd
fixed bug when empty dict is present in params
MahdiAll99 Mar 6, 2026
315f3a7
fixed missing cols bug
MahdiAll99 Mar 8, 2026
3f97cb3
reversed pycaret version change (avoid risk for POC1)
MahdiAll99 Mar 8, 2026
cd7b125
Merge pull request #548 from MEDomicsLab/learningFixes
MahdiAll99 Mar 8, 2026
5eaa731
fixed macOS issues in superset env and superset service kill
MahdiAll99 Mar 9, 2026
6d29159
Merge pull request #549 from MEDomicsLab/ssFixes
MahdiAll99 Mar 9, 2026
d94f6fd
Merge branch 'learningFixes' of github.com:MEDomics-UdeS/MEDomicsLab …
Mar 10, 2026
b2540ac
Fixed optimize_threshold problems (front and back)
Mar 16, 2026
40a19fc
added base files to resources
MahdiAll99 Apr 8, 2026
6866fae
fix in conda env path in settings
MahdiAll99 Apr 9, 2026
c08bf3a
renamed prediction score to confidence score
MahdiAll99 Apr 9, 2026
1343887
script kill bug fix
MahdiAll99 Apr 9, 2026
341f242
error handling bug fix and added stack trace
MahdiAll99 Apr 9, 2026
b48eaea
Improved error handling, errors outside start() were not correctly ha…
MahdiAll99 Apr 9, 2026
60ed7b2
Progress bar message update
MahdiAll99 Apr 9, 2026
04db9df
Fix: avoid duplicate imports
MahdiAll99 Apr 10, 2026
b2ebdb2
Huge fix on code generation feature and final model training (use act…
MahdiAll99 Apr 10, 2026
2c9a050
Unused function: set_model
MahdiAll99 Apr 10, 2026
e035f19
Seperated useEffects to better handle changes in python or conda env …
MahdiAll99 Apr 10, 2026
57118f1
minor comment fix
MahdiAll99 Apr 10, 2026
db9ac0a
Merge pull request #550 from MEDomicsLab/learningFixes
MahdiAll99 Apr 10, 2026
7d3d335
added ml algo to model viewer
MahdiAll99 Apr 11, 2026
ec1b549
bug fix: using two models or more was not working
MahdiAll99 Apr 11, 2026
4f00f02
Merge pull request #551 from MEDomicsLab/learningFixes
MahdiAll99 Apr 11, 2026
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
4 changes: 4 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ extraResources: # Add this section if it doesn't exist, or add the line to it
- "**/*"
- "!**/.git"
- "!**/.git/**"
- from: ./baseFiles # Source directory
to: baseFiles # Destination directory inside Contents/Resources
filter:
- emptyNotebook.ipynb

extends: null # Added from package.json
mac: # Added from package.json
Expand Down
153 changes: 115 additions & 38 deletions go_server/src/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,30 +163,20 @@ func StartPythonScripts(jsonParam string, filename string, id string) (string, e
}
log.Println("Conda env: " + condaEnv)

// UNCOMMENT TO WRITE JSON PARAM TO FILE FOR DEBUGGING
// jsonParamBytes := []byte(jsonParam)
// err = os.WriteFile("jsonParam.txt", jsonParamBytes, 0644)
// if err != nil {
// log.Println("Error writing jsonParam to file")
// return "", err
// }

Scripts[id] = ScriptInfo{
Cmd: exec.Command(condaEnv, "-u", script, "--json-param", jsonParam, "--id", id),
Progress: "",
}
stdout, err := Scripts[id].Cmd.StdoutPipe()
Mu.Unlock()
if err != nil {
log.Println("Error getting stdout pipe")
log.Println(err.Error())
log.Println("Error getting stdout pipe: " + err.Error())
}
Mu.Lock()
stderr, err := Scripts[id].Cmd.StderrPipe()
Mu.Unlock()
if err != nil {
log.Println("Error getting stderr pipe")
log.Println(err.Error())
log.Println("Error getting stderr pipe: " + err.Error())
}
Mu.Lock()
err = Scripts[id].Cmd.Start()
Expand All @@ -195,51 +185,138 @@ func StartPythonScripts(jsonParam string, filename string, id string) (string, e
log.Println("Error starting command " + Scripts[id].Cmd.String())
return "", err
}

response := ""
go copyOutput(stdout, &response)
go copyOutput(stderr, &response)
err = Scripts[id].Cmd.Wait()
if err != nil {
log.Println("Error waiting for command to finish")
return "", err
// stderrBuf collects every line from stderr so we can surface it on early
// crashes (e.g. ModuleNotFoundError) that never reach send_response().
var stderrBuf strings.Builder
var stderrMu sync.Mutex

var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
copyOutput(stdout, &response)
}()
go func() {
defer wg.Done()
copyOutputErr(stderr, &response, &stderrBuf, &stderrMu)
}()
wg.Wait()

waitErr := Scripts[id].Cmd.Wait()

// Happy path – the Python layer called send_response() successfully.
if response != "" {
log.Println("Finished running script: " + filename + " with id: " + id)
return response, nil
}
log.Println("Finished running script: " + filename + " with id: " + id)
return response, nil

// The script exited without ever emitting response-ready (early crash).
// Build a structured error JSON that matches get_response_from_error() so
// the frontend receives consistent error objects regardless of where in
// Python the failure happened.
stderrMu.Lock()
capturedStderr := stderrBuf.String()
stderrMu.Unlock()

exitMsg := "unknown error"
if waitErr != nil {
exitMsg = waitErr.Error()
}
if capturedStderr == "" {
capturedStderr = exitMsg
}

log.Println("Script crashed before send_response. stderr:\n" + capturedStderr)

errorPayload := map[string]interface{}{
"error": map[string]string{
"message": exitMsg,
"stack_trace": capturedStderr,
"value": capturedStderr,
},
}
jsonBytes, jsonErr := json.Marshal(errorPayload)
if jsonErr != nil {
// Last-resort fallback – should never happen.
return "", waitErr
}
return string(jsonBytes), nil
}

// It is used to transfer stdout and stderr to the terminal
// copyOutput handles stdout: detects response-ready and progress signals.
func copyOutput(r io.Reader, response *string) {
scanner := bufio.NewScanner(r)
lineText := ""
for scanner.Scan() {
lineText = scanner.Text()
lineText := scanner.Text()
if strings.Contains(lineText, "response-ready*_*") {
path := strings.Split(lineText, "*_*")[1]
path = path[:len(path)-1]
*response = ReadFile(path)
// delete this file
err := os.Remove(path)
if err != nil {
if err := os.Remove(path); err != nil {
log.Println(err)
}
} else if strings.Contains(lineText, "progress*_*") {
id := strings.Split(lineText, "*_*")[1]
progress := strings.Split(lineText, "*_*")[2]
progress = progress[:len(progress)-1]
log.Println("Progress: " + progress)
Mu.Lock()
Scripts[id] = ScriptInfo{
Cmd: Scripts[id].Cmd,
Progress: progress,
parts := strings.Split(lineText, "*_*")
if len(parts) >= 3 {
pid := parts[1]
progress := parts[2][:len(parts[2])-1]
log.Println("Progress: " + progress)
Mu.Lock()
Scripts[pid] = ScriptInfo{
Cmd: Scripts[pid].Cmd,
Progress: progress,
}
Mu.Unlock()
}
}
}
}

// copyOutputErr mirrors copyOutput for stderr.
// It still watches for response-ready/progress (some Python loggers write to
// stderr) and additionally accumulates every line into stderrBuf so early
// crashes (ImportError, SyntaxError, …) are never silently dropped.
func copyOutputErr(r io.Reader, response *string, stderrBuf *strings.Builder, mu *sync.Mutex) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lineText := scanner.Text()
log.Println("[stderr] " + lineText)

// Accumulate for crash reporting.
mu.Lock()
stderrBuf.WriteString(lineText + "\n")
mu.Unlock()

// Handle structured signals even if they arrive on stderr.
if strings.Contains(lineText, "response-ready*_*") {
path := strings.Split(lineText, "*_*")[1]
path = path[:len(path)-1]
*response = ReadFile(path)
if err := os.Remove(path); err != nil {
log.Println(err)
}
} else if strings.Contains(lineText, "progress*_*") {
parts := strings.Split(lineText, "*_*")
if len(parts) >= 3 {
pid := parts[1]
progress := parts[2][:len(parts[2])-1]
Mu.Lock()
Scripts[pid] = ScriptInfo{
Cmd: Scripts[pid].Cmd,
Progress: progress,
}
Mu.Unlock()
}
Mu.Unlock()
}
}
}

// ReadFile reads a file and returns its content as a string
func ReadFile(filename string) string {
absPath, _ := filepath.Abs(filename)
cleanPath := strings.Trim(filename, "\" \t\n\r")
absPath, _ := filepath.Abs(cleanPath)
log.Println("Reading file: " + absPath)
data, err := os.ReadFile(absPath)
if err != nil {
Expand Down Expand Up @@ -295,8 +372,8 @@ func KillScript(id string) bool {
}
}()
if script.Cmd != nil { // Check if script.Cmd is not nil
if script.Cmd.ProcessState != nil && script.Cmd.ProcessState.Exited() {
log.Println("Script can be killed")
if script.Cmd.Process != nil && (script.Cmd.ProcessState == nil || !script.Cmd.ProcessState.Exited()) {
log.Println("Script is running, killing it now...")
err := script.Cmd.Process.Kill()
if err != nil {
log.Print("Error killing process: ", err.Error())
Expand Down
35 changes: 33 additions & 2 deletions pythonCode/med_libs/MEDml/MEDexperiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(self, global_json_config: json = None):
self.finalize_node = global_json_config.get('modelToFinalize', None)
self.finalize_is_combine = self.__get_node_type(self.finalize_node) == 'combine_models' if self.finalize_node is not None else False
self.pipelines = self.__init_pipelines(global_json_config['pipelines'])
self.pipelines = self.split_pipelines(self.pipelines)
self.pipelines_to_execute = self.pipelines
self._results_pipeline = {}
self._progress = {'currentLabel': '', 'now': 0.0}
Expand Down Expand Up @@ -106,6 +107,30 @@ def __get_node_type(self, node_id: str) -> str:
else:
raise ValueError(f"Node {node_id} not found in global json config.")

def split_pipelines(self, data):
if not isinstance(data, dict):
return data

new_dict = {}
for key, value in data.items():
if "." in key:
# Split the key into the part before and after the dot
first_part, second_part = key.split(".", 1)

# Extract the prefix before the '*' to reconstruct the second key
prefix = first_part.split("*")[0] + "*"
reconstructed_second_key = prefix + second_part

# Recursively process the children for both new keys
processed_value = self.split_pipelines(value)
new_dict[first_part] = processed_value
new_dict[reconstructed_second_key] = processed_value
else:
# Recurse deeper into the dictionary
new_dict[key] = self.split_pipelines(value)

return new_dict

def update(self, global_json_config: json = None):
"""Updates the experiment with the pipelines and the global configuration.

Expand Down Expand Up @@ -199,7 +224,10 @@ def start(self) -> None:
for current_node_id, next_nodes_id_json in self.pipelines_to_execute.items():
node_info = self.pipelines_objects[current_node_id]
node: Node = node_info['obj']
self._progress['currentLabel'] = node.username
if node.username.lower() == 'train model':
self._progress['currentLabel'] = f"Training model (This process may take long)"
else:
self._progress['currentLabel'] = node.username
has_been_run = node.has_run()
if not has_been_run or 'experiment' not in node_info:
node_info['results'] = {
Expand Down Expand Up @@ -283,7 +311,10 @@ def execute_next_nodes(
node = node_info['obj']
experiment = self.copy_experiment(experiment)
exp_to_return = experiment
self._progress['currentLabel'] = node.username
if node.username.lower() == 'train model':
self._progress['currentLabel'] = f"Training model (This process may take long)"
else:
self._progress['currentLabel'] = node.username
if not node.has_run() or prev_node.has_changed():
if node.type == 'group_models':
print("group_models")
Expand Down
Loading
Loading