Erdnaxela3/bioptim_gui

View on GitHub
gui/lib/models/python_interface.dart

Summary

Maintainability
Test Coverage
import 'dart:developer';
import 'dart:io';

import 'package:flutter/services.dart';

enum PythonInterfaceStatus {
  uninitialized,
  initializing,
  failedAlreadyInitializing,
  failedMissingAnaconda,
  failedCreatingEnvironment,
  failedInstallingBioptim,
  failedUnknown,
  ready,
  isRunning,
}

class PythonInterface {
  static final PythonInterface _instance = PythonInterface._internal();
  static PythonInterface get instance => _instance;

  void registerToStatusChanged(Function(PythonInterfaceStatus) callback) =>
      _onStatusChangedCallbacks.add(callback);
  final List<Function(PythonInterfaceStatus status)> _onStatusChangedCallbacks =
      [];

  // TODO add common fix tips for user (such as running the app inside anaconda)
  String _workingDirectory = '.';
  bool _skipEnvironmentLoading = false;
  String? _environment;
  PythonInterfaceStatus _status = PythonInterfaceStatus.uninitialized;
  PythonInterfaceStatus get status => _status;
  set status(PythonInterfaceStatus value) {
    _status = value;
    for (final callback in _onStatusChangedCallbacks) {
      callback(_status);
    }
  }

  Future<Process?> runCreateGraph(
    List<String> picklePaths, {
    String? overrideWorkingDirectory,
  }) async {
    if (status != PythonInterfaceStatus.ready) return null;
    status = PythonInterfaceStatus.isRunning;

    Future<void> changeStatusWhenDone(Process process) async {
      await process.exitCode;
      status = PythonInterfaceStatus.ready;
    }

    List<String> pathAndArgs = [
      'graph_multiple_sols.py',
    ];

    for (final picklePath in picklePaths) {
      pathAndArgs.add(picklePath);
    }

    final process = await Process.start(
        '${_skipEnvironmentLoading ? '' : _loadEnvironmentCommand}python',
        pathAndArgs,
        runInShell: true,
        workingDirectory: overrideWorkingDirectory ?? _workingDirectory);

    changeStatusWhenDone(process);
    return process;
  }

  Future<Process?> runAnimateSolution(
    String picklePath, {
    String? overrideWorkingDirectory,
  }) async {
    if (status != PythonInterfaceStatus.ready) return null;
    status = PythonInterfaceStatus.isRunning;

    Future<void> changeStatusWhenDone(Process process) async {
      await process.exitCode;
      status = PythonInterfaceStatus.ready;
    }

    final pathAndArgs = [
      'animate.py',
      picklePath,
    ];

    final process = await Process.start(
        '${_skipEnvironmentLoading ? '' : _loadEnvironmentCommand}python',
        pathAndArgs,
        runInShell: true,
        workingDirectory: overrideWorkingDirectory ?? _workingDirectory);

    changeStatusWhenDone(process);
    return process;
  }

  Future<Process?> runFile(
    String path, {
    bool multistart = false,
    int nbSeeds = 1,
    String? overrideWorkingDirectory,
  }) async {
    // TODO Better fail if not ready (popup?)
    if (status != PythonInterfaceStatus.ready) return null;
    status = PythonInterfaceStatus.isRunning;

    Future<void> changeStatusWhenDone(Process process) async {
      await process.exitCode;
      status = PythonInterfaceStatus.ready;
    }

    final pathAndArgs = multistart
        ? [
            path,
            '-m',
            nbSeeds.toString(),
          ]
        : [
            path,
          ];

    final process = await Process.start(
        '${_skipEnvironmentLoading ? '' : _loadEnvironmentCommand}python',
        pathAndArgs,
        runInShell: true,
        workingDirectory: overrideWorkingDirectory ?? _workingDirectory);

    changeStatusWhenDone(process);
    return process;
  }

  Future<PythonInterfaceStatus> initialize(
      {String? workingDirectory, String? environment}) async {
    if (status == PythonInterfaceStatus.initializing) {
      // Do not initalize twice
      return PythonInterfaceStatus.failedAlreadyInitializing;
    }

    // Setup some internal values
    _workingDirectory = workingDirectory ?? '.';
    _environment = environment;
    status = PythonInterfaceStatus.initializing;

    // If bioptim is already installed there is no need to do anything else
    if (await _isBioptimInstalled(skipEnvironmentLoading: true)) {
      _skipEnvironmentLoading = true;
      status = PythonInterfaceStatus.ready;
      return status;
    }

    if (await _isBioptimInstalled()) {
      status = PythonInterfaceStatus.ready;
      return status;
    }

    // Otherwise, try to install it using anaconda
    if (!(await _isAnacondaInstalled())) {
      status = PythonInterfaceStatus.failedMissingAnaconda;
      return status;
    }

    // Check if current installation was made by this script
    if (!(await _isCondaEnvironmentInstalled())) {
      if (!(await _installCondaEnvironment())) {
        status = PythonInterfaceStatus.failedCreatingEnvironment;
        return status;
      }
    }

    if (!(await _installBioptim())) {
      status = PythonInterfaceStatus.failedInstallingBioptim;
      return status;
    }

    if (!(await _isBioptimInstalled())) {
      status = PythonInterfaceStatus.failedUnknown;
      return status;
    }

    status = PythonInterfaceStatus.ready;
    return status;
  }

  PythonInterface._internal();

  String get _loadEnvironmentCommand =>
      _environment == null ? '' : 'conda activate $_environment && ';

  void _logProcess(String functionName, ProcessResult result) {
    if (result.exitCode != 0) {
      log('$functionName failed with error:\n${result.stderr}');
    } else {
      log('$functionName success');
    }
  }

  Future<bool> _isAnacondaInstalled() async {
    final result = await Process.run('conda', ['-V'],
        runInShell: true, workingDirectory: _workingDirectory);
    _logProcess('isAnacondaInstalled', result);
    return result.exitCode == 0;
  }

  Future<bool> _isCondaEnvironmentInstalled() async {
    final result = await Process.run('conda', ['env', 'list'],
        runInShell: true, workingDirectory: _workingDirectory);
    _logProcess('isCondaEnvironmentInstalled', result);
    return result.stdout.contains(_environment);
  }

  Future<bool> _installCondaEnvironment() async {
    final result = await Process.run(
        'conda', ['create', '-n$_environment', '-y'],
        runInShell: true, workingDirectory: _workingDirectory);
    _logProcess('installCondaEnvironment', result);
    return result.exitCode == 0;
  }

  Future<bool> _isBioptimInstalled(
      {bool skipEnvironmentLoading = false}) async {
    // First we have to get the test file from the assets folder
    const bioptimTesterFileName = 'bioptim_tester.py';
    final newBioptimTester = File('$_workingDirectory/$bioptimTesterFileName');
    newBioptimTester.writeAsString(
        await rootBundle.loadString('assets/$bioptimTesterFileName'));

    // Then we can run the file to test if bioptim is installed
    final result = await Process.run(
        '${skipEnvironmentLoading ? '' : _loadEnvironmentCommand}python',
        [bioptimTesterFileName],
        runInShell: true,
        workingDirectory: _workingDirectory);
    _logProcess('isBioptimInstalled', result);

    // Now clean the mess
    newBioptimTester.delete();

    return result.exitCode == 0;
  }

  Future<bool> _installBioptim() async {
    final result = await Process.run('${_loadEnvironmentCommand}conda',
        ['install', '-cconda-forge', 'bioptim', '-y'],
        runInShell: true, workingDirectory: _workingDirectory);
    _logProcess('installBioptim', result);
    return result.exitCode == 0;
  }
}