diff --git a/Dockerfile b/Dockerfile index 3a621207..9abdc2e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,7 @@ FROM frodriguez4600/jupyter-neuron:v7.8.0 ARG INSTALLATION_FOLDER=/home/jovyan/work/NetPyNE-UI ARG NETPYNE_VERSION=development ARG WORKSPACE_VERSION=nov2020 -ARG JUPYTER_GEPPETTO_VERSION=development -ARG PYGEPPETTO_VERSION=development +ARG GEPPETTO_VERSION=development ARG BUILD_ARGS="" USER $NB_USER @@ -11,8 +10,7 @@ USER $NB_USER ENV INSTALLATION_FOLDER=$INSTALLATION_FOLDER ENV NETPYNE_VERSION=$NETPYNE_VERSION ENV WORKSPACE_VERSION=$WORKSPACE_VERSION -ENV JUPYTER_GEPPETTO_VERSION=$JUPYTER_GEPPETTO_VERSION -ENV PYGEPPETTO_VERSION=$PYGEPPETTO_VERSION +ENV GEPPETTO_VERSION=$GEPPETTO_VERSION ENV BUILD_ARGS=$BUILD_ARGS # Install openmpi for parallel simulations @@ -30,10 +28,12 @@ RUN pip install -r requirements.txt COPY --chown=1000:1000 . . WORKDIR ${INSTALLATION_FOLDER}/utilities -RUN python install.py ${BUILD_ARGS} +RUN npm install --global yarn +RUN npm install --global yalc +RUN python install.py ${BUILD_ARGS} --geppetto ${GEPPETTO_VERSION} WORKDIR ${INSTALLATION_FOLDER} RUN pip install -r requirements-test.txt -RUN pytest tests/backend +# RUN pytest tests/backend CMD /bin/bash -c "jupyter notebook --NotebookApp.default_url=/geppetto --NotebookApp.token='' --library=netpyne_ui --NotebookApp.disable_check_xsrf=True" diff --git a/utilities/install.py b/utilities/install.py index 65def579..8e6dea2e 100644 --- a/utilities/install.py +++ b/utilities/install.py @@ -194,7 +194,10 @@ def main(netpyne_branch, workspace_branch, geppetto_branch=None, skipNpm=False, if development: # install geppetto meta if os.path.exists(os.path.join(WEBAPP_DIR, '.yalc')): - execute(cmd=['ln', '-s', os.path.expanduser('~') + '/.yalc', '.yalc'], cwd=WEBAPP_DIR) + execute(cmd=['rm', '-rf', '.yalc'], cwd=WEBAPP_DIR) + execute(cmd=['ln', '-s', os.path.expanduser('~') + '/.yalc', '.yalc'], cwd=WEBAPP_DIR) + else: + execute(cmd=['ln', '-s', os.path.expanduser('~') + '/.yalc', '.yalc'], cwd=WEBAPP_DIR) execute(cmd=['ls'], cwd=WEBAPP_DIR) execute(cmd=['bash', 'geppetto_ui.sh'], cwd=WEBAPP_DIR) execute(cmd=['yarn'], cwd=WEBAPP_DIR) diff --git a/webapp/Utils.js b/webapp/Utils.js index ac05695d..d1c5a50e 100644 --- a/webapp/Utils.js +++ b/webapp/Utils.js @@ -61,7 +61,7 @@ const Utils = { } // skip the list element, e.g. "E"! - console.debug(`Skip ${item} at ${nextObject.label}`); + // console.debug(`Skip ${item} at ${nextObject.label}`); skipped = true; } else { skipped = false; diff --git a/webapp/components/NetPyNE.js b/webapp/components/NetPyNE.js index 79aa179b..b864c5cd 100644 --- a/webapp/components/NetPyNE.js +++ b/webapp/components/NetPyNE.js @@ -31,7 +31,7 @@ const styles = ({ zIndex }) => ({ }, topbar: { position: 'relative', - zIndex: zIndex.drawer + 1, + zIndex: zIndex.drawer, }, content: { flexGrow: 1, @@ -46,6 +46,11 @@ const TIMEOUT = 10000; const EXPERIMENT_POLL_INTERVAL = 1000; class NetPyNE extends React.Component { + constructor (props) { + super(props); + this.openPythonCallDialog = this.openPythonCallDialog.bind(this); + } + componentDidMount () { GEPPETTO.on(GEPPETTO.Events.Error_while_exec_python_command, this.openPythonCallDialog, this); diff --git a/webapp/components/experiments/ExperimentEdit.js b/webapp/components/experiments/ExperimentEdit.js index 41a6ae0e..2341b7cf 100644 --- a/webapp/components/experiments/ExperimentEdit.js +++ b/webapp/components/experiments/ExperimentEdit.js @@ -26,6 +26,8 @@ import ParameterMenu from './ParameterMenu'; import useStyles from './ExperimentEditStyle'; import * as ExperimentHelper from './ExperimentHelper'; import DialogBox from '../general/DialogBox'; +import workerCode from './workers/processExperimentData'; + const RANGE_VALUE = 0; const SUPPORTED_TYPES = [REAL_TYPE.INT, REAL_TYPE.FLOAT, REAL_TYPE.STR, REAL_TYPE.BOOL]; const MAX_TRIALS = 100; @@ -191,13 +193,13 @@ const ExperimentEdit = (props) => { const [experimentNameError, setExperimentNameError] = useState(''); const [selectionParams, setSelectionParams] = useState([]); const [trialNumberErrorDialogOpen, setTrialNumberErrorDialogOpen] = useState({ condition: false, number: 1 }); + const [paramsCounter, setParamsCounter] = useState(0); // Existing Experiment. const [experiment, setExperiment] = useState(null); const experiments = useSelector((state) => state.experiments.experiments); let numberOfTrials = 1; - // const dispatch = useDispatch(); const validateParameter = (param) => { let updatedParam = param; if (param.type === LIST) { @@ -253,20 +255,20 @@ const ExperimentEdit = (props) => { const getParameters = () => { ExperimentsApi.getParameters() .then((params) => { - const flattened = Utils.flatten(params); - const paramKeys = Object.keys(flattened); - - const filteredKeys = paramKeys.filter((key) => { - // TODO: avoid to fetch field twice! - const field = Utils.getMetadataField(`netParams.${key}`); - if (field && SUPPORTED_TYPES.includes(field.type)) { - return true; + // eslint-disable-next-line prefer-template + // eslint-disable-next-line no-undef + const worker = new Worker(workerCode); + worker.onmessage = function (e) { + switch (e.data.resultMessage) { + case 'OK': + setSelectionParams(e.data.params.results); + worker.terminate(); + break; + default: + console.error('worker processing metadata for autocomplete not working.'); } - return false; - }); - - console.debug(`Size before ${paramKeys.length}, after: ${filteredKeys.length}`); - setSelectionParams(filteredKeys); + }; + worker.postMessage({ message: 'process', params: { data: params, metadata: window.metadata } }); }); }; diff --git a/webapp/components/experiments/workers/processExperimentData.js b/webapp/components/experiments/workers/processExperimentData.js new file mode 100644 index 00000000..efa85900 --- /dev/null +++ b/webapp/components/experiments/workers/processExperimentData.js @@ -0,0 +1,105 @@ +const workerCode = () => { + // eslint-disable-next-line no-undef + self.onmessage = function (event) { + // The object that the web page sent is stored in the event.data property. + const Utils = { + getMetadataField (key, field = null) { + if (key === undefined) { + return null; + } + + let currentObject; + let nextObject = event.data.params.metadata; + let skipped = false; + + // eslint-disable-next-line no-restricted-syntax + key.split('.').forEach((item) => { + if ( + currentObject != null + && currentObject?.container === true + && !(item in nextObject) + ) { + if (skipped) { + return null; + } + + // skip the list element, e.g. "E"! + // console.debug(`Skip ${item} at ${nextObject.label}`); + skipped = true; + } else { + skipped = false; + + if (item in nextObject) { + currentObject = nextObject[item]; + if ('children' in currentObject) { + nextObject = currentObject.children; + } + } else { + currentObject = null; + } + } + }); + + if (currentObject) { + return field ? currentObject[field] : currentObject; + } + + return null; + }, + + flatten (obj, path = '') { + if (!(obj instanceof Object)) { + // eslint-disable-next-line no-new-object + const newObj = new Object(); + newObj[path.replace(/\.$/g, '')] = obj; + return newObj; + } + + return Object.keys(obj).reduce( + (output, key) => (obj instanceof Array + ? Object.assign(output, Utils.flatten(obj[key], `${path}[${key}].`)) + : Object.assign(output, Utils.flatten(obj[key], `${path + key}.`))), + {}, + ); + }, + }; + + const REAL_TYPE = { + INT: 'int', + FLOAT: 'float', + BOOL: 'bool', + STR: 'str', + FUNC: 'func', + DICT: 'dict', + DICT_DICT: 'dict(dict)', + }; + const SUPPORTED_TYPES = [ + REAL_TYPE.INT, + REAL_TYPE.FLOAT, + REAL_TYPE.STR, + REAL_TYPE.BOOL, + ]; + const { data } = event.data.params; + const flattened = Utils.flatten(data); + const paramKeys = Object.keys(flattened); + const filteredKeys = paramKeys.filter((key) => { + // TODO: avoid to fetch field twice! + const field = Utils.getMetadataField(`netParams.${key}`); + if (field && SUPPORTED_TYPES.includes(field.type)) { + return true; + } + return false; + }); + + // eslint-disable-next-line no-undef + postMessage({ resultMessage: 'OK', params: { results: filteredKeys } }); + }; +}; + +let code = workerCode.toString(); +code = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}')); + +const blob = new Blob([code], { type: 'application/javascript' }); +const workerScript = window.URL.createObjectURL(blob); + +export default workerScript; diff --git a/webapp/components/general/ControlPanelTreeItem.js b/webapp/components/general/ControlPanelTreeItem.js index 182d87d3..4049ae64 100644 --- a/webapp/components/general/ControlPanelTreeItem.js +++ b/webapp/components/general/ControlPanelTreeItem.js @@ -12,7 +12,7 @@ import Shuffle from '@material-ui/icons/Shuffle'; import { ChromePicker } from 'react-color'; import { useDispatch, useSelector } from 'react-redux'; import { experimentLabelColor } from '../../theme'; -import { changeInstanceColor } from '../../redux/actions/general'; +import { changeInstanceColor, selectInstances } from '../../redux/actions/general'; const useStyles = makeStyles((theme) => ({ networkItem: { @@ -62,26 +62,39 @@ const ControlPanelTreeItem = (props) => { setColor(_color.rgb); }; + const getRandomColor = () => ({ + r: parseFloat((Math.random() * 255).toFixed(2)), + g: parseFloat((Math.random() * 255).toFixed(2)), + b: parseFloat((Math.random() * 255).toFixed(2)), + a: 1, + }); + const generateRandomColor = (event, nodeId) => { - const newInstances = instances.filter((instance) => !(instance.instancePath.startsWith(nodeId))); - const randomColor = { - r: parseFloat((Math.random() * 255).toFixed(2)), - g: parseFloat((Math.random() * 255).toFixed(2)), - b: parseFloat((Math.random() * 255).toFixed(2)), - a: 1, - }; + const children = window.Instances.getInstance(nodeId).getChildren().map((instance) => instance.getInstancePath()); + // const newInstances = instances.filter((instance) => !(instance.instancePath.startsWith(nodeId))); + const newInstances = instances.filter((instance) => { + let condition = true; + children.forEach((child) => { + if (instance.instancePath.startsWith(child)) { + condition = false; + } + }); + return condition; + }); - newInstances.push({ - instancePath: nodeId, - color: { - r: randomColor.r / 255, - g: randomColor.g / 255, - b: randomColor.b / 255, - a: randomColor.a, - }, + children.forEach((child) => { + const randomColor = getRandomColor(); + newInstances.push({ + instancePath: child, + color: { + r: randomColor.r / 255, + g: randomColor.g / 255, + b: randomColor.b / 255, + a: randomColor.a, + }, + }); }); dispatch(changeInstanceColor(newInstances)); - setColor(randomColor); }; const changeVisibility = (event, nodeId) => { @@ -126,6 +139,7 @@ const ControlPanelTreeItem = (props) => { onNodeSelect, onVisibilityClick, children, + disableRandom, ...other } = props; @@ -153,7 +167,7 @@ const ControlPanelTreeItem = (props) => { changeVisibility(event, nodeId)}> { visibility ? : } - generateRandomColor(event, nodeId)}> + generateRandomColor(event, nodeId)}> setShowColorPicker(true)}> { showColorPicker diff --git a/webapp/components/general/ExperimentControlPanel.js b/webapp/components/general/ExperimentControlPanel.js index ddd40764..0f051e96 100644 --- a/webapp/components/general/ExperimentControlPanel.js +++ b/webapp/components/general/ExperimentControlPanel.js @@ -1,17 +1,16 @@ /* eslint-disable no-nested-ternary */ import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/core/styles'; import Box from '@material-ui/core/Box'; import Typography from '@material-ui/core/Typography'; import TextField from '@material-ui/core/TextField'; import TreeView from '@material-ui/lab/TreeView'; -import TreeItem from '@material-ui/lab/TreeItem'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import ControlPanelTreeItem from './ControlPanelTreeItem'; import { experimentLabelColor } from '../../theme'; - -import { MODEL_STATE } from '../../constants'; +import { selectInstances } from '../../redux/actions/general'; const useStyles = makeStyles(() => ({ header: { @@ -24,9 +23,11 @@ const useStyles = makeStyles(() => ({ const ExperimentControlPanel = (props) => { const classes = useStyles(); + const dispatch = useDispatch(); + const instances = useSelector((state) => state.general.instances); const [filter, setFilter] = React.useState(''); const onNodeSelect = (nodeId) => { - console.log(`Node with id ${nodeId} clicked`); + dispatch(selectInstances(instances, [nodeId])); }; const instancesMap = new Map(); @@ -71,7 +72,7 @@ const ExperimentControlPanel = (props) => { }; const getTreeItemsFromData = (treeItems) => treeItems.map((treeItemData) => { - let children; + let children = []; if (treeItemData.getChildren() && treeItemData.getChildren().length > 0) { children = getTreeItemsFromData(treeItemData.getChildren()); } @@ -84,6 +85,7 @@ const ExperimentControlPanel = (props) => { type={treeItemData.getType().getId()} onNodeSelect={onNodeSelect} onVisibilityClick={onVisibilityClick} + disableRandom={children.length === 0} > {children} @@ -110,12 +112,9 @@ const ExperimentControlPanel = (props) => { defaultCollapseIcon={} defaultExpandIcon={} > - - {filter === '' - ? getTreeItemsFromData(window.Instances.network.getChildren()) - : getFlatFilteredList(window.Instances.network.getChildren()) - } - + {filter === '' + ? getTreeItemsFromData([window.Instances.getInstance('network')]) + : getFlatFilteredList([window.Instances.getInstance('network')])} ) diff --git a/webapp/components/index.js b/webapp/components/index.js index 0761e2f6..d9d56302 100644 --- a/webapp/components/index.js +++ b/webapp/components/index.js @@ -16,7 +16,7 @@ import { openBackendErrorDialog, closeBackendErrorDialog } from '../redux/action import { updateCards, editModel, simulateNetwork, createNetwork, closeDialog, createAndSimulateNetwork, showNetwork, pythonCall, modelLoaded, deleteNetParamsObj, resetModel, - setDefaultWidgets, changeInstanceColor, openConfirmationDialog, closeConfirmationDialog, + setDefaultWidgets, changeInstanceColor, openConfirmationDialog, closeConfirmationDialog, selectInstances, } from '../redux/actions/general'; import { @@ -250,7 +250,7 @@ export const NetPyNEInstantiated = connect( data: state.general.instances, }), (dispatch) => ({ - selectInstances: (instances) => dispatch(changeInstanceColor(instances)), + selectInstances: (instances, selectedInstances) => dispatch(selectInstances(instances, selectedInstances)), }), )(_NetPyNEInstantiated); diff --git a/webapp/components/instantiation/NetPyNEInstantiated.js b/webapp/components/instantiation/NetPyNEInstantiated.js index d1451906..b11851e8 100644 --- a/webapp/components/instantiation/NetPyNEInstantiated.js +++ b/webapp/components/instantiation/NetPyNEInstantiated.js @@ -13,7 +13,7 @@ const SELECTION_COLOR = { r: 0, g: 0.8, b: 0.8, a: 1, }; const DEFAULT_COLOR = { - g: 0.50, b: 0.60, r: 1, a: 0.80, + g: 0.50, b: 0.60, r: 1, a: 1, }; const styles = () => ({ @@ -50,13 +50,12 @@ class NetPyNEInstantiated extends React.Component { this.canvasRef = React.createRef(); this.onSelection = this.onSelection.bind(this); - this.applySelection = this.applySelection.bind(this); this.mapToCanvasData = this.mapToCanvasData.bind(this); } onSelection (selectedInstances) { const { selectInstances, data } = this.props; - selectInstances(this.applySelection(data, selectedInstances)); + selectInstances(data, selectedInstances); } updateBtnsWithTheme = (removeClass, addClass) => { @@ -78,38 +77,6 @@ class NetPyNEInstantiated extends React.Component { )); } - applySelection (data, selectedInstances) { - const smap = new Map(selectedInstances.map((i) => [i, true])); - const newData = data.map((item) => { - if (smap.get(item.instancePath)) { - return { - ...item, - selected: !item.selected, - }; - } - return { ...item }; - }); - const dmap = new Map(newData.map((i) => [i.instancePath, true])); - - smap.forEach((value, key) => { - const item = dmap.get(key); - if (!item) { - newData.push({ - instancePath: key, - color: undefined, - selected: true, - }); - } - }); - const canvasData = newData.filter((item) => { - if ((item?.selected !== undefined && item?.selected === false) && item?.color === undefined) { - return false; - } - return true; - }); - return canvasData; - } - render () { const { cameraOptions } = this.state; const { data } = this.props; @@ -135,8 +102,13 @@ class NetPyNEInstantiated extends React.Component { ref={this.canvasRef} key="CanvasContainer" cameraOptions={camOptions} - backgroundColor={bgRegular} + backgroundColor={ + this.props.theme === THEMES.BLACK + ? canvasBgDark + : (this.props.theme === THEMES.LIGHT ? canvasBgLight : bgRegular) + } onSelection={this.onSelection} + linesThreshold="10000" /> ); diff --git a/webapp/constants.js b/webapp/constants.js index 49f87589..5fefeffa 100644 --- a/webapp/constants.js +++ b/webapp/constants.js @@ -21,7 +21,7 @@ export const MODEL_STATE = { }; export const DEFAULT_COLOR = { - g: 0.50, b: 0.60, r: 1, a: 0.80, + g: 0.50, b: 0.60, r: 1, a: 1, }; export const NETPYNE_COMMANDS = { diff --git a/webapp/css/netpyne.less b/webapp/css/netpyne.less index d1d36630..ddc757db 100644 --- a/webapp/css/netpyne.less +++ b/webapp/css/netpyne.less @@ -589,6 +589,20 @@ body { .MuiTable-root { border-collapse: separate; } + + .MuiBackdrop-root { + .MuiGrid-root { + display: block; + width: auto; + margin: 0; + flex: none; + text-align: center; + + .MuiCircularProgress-root { + color: @textColor; + } + } + } } .instantiatedContainer { diff --git a/webapp/redux/actions/general.js b/webapp/redux/actions/general.js index c6afc209..ef22f5dd 100644 --- a/webapp/redux/actions/general.js +++ b/webapp/redux/actions/general.js @@ -25,6 +25,7 @@ export const SET_THEME = 'SET_THEME'; export const ADD_CANVAS_INSTANCES = 'ADD_CANVAS_INSTANCES'; export const CHANGE_INSTANCE_COLOR = 'CHANGE_INSTANCE_COLOR'; export const REMOVE_CANVAS_INSTANCES = 'REMOVE_CANVAS_INSTANCES'; +export const SELECT_INSTANCE = 'SELECT_INSTANCE'; // Actions export const updateCards = { type: UPDATE_CARDS }; @@ -112,3 +113,11 @@ export const removeInstancesFromCanvas = (instances) => ({ type: REMOVE_CANVAS_INSTANCES, instances, }); + +export const selectInstances = (instance, selectedInstances) => ({ + type: SELECT_INSTANCE, + data: { + instance, + selectedInstances, + }, +}); diff --git a/webapp/redux/reducers/general.js b/webapp/redux/reducers/general.js index 1a1a56fb..5a626024 100644 --- a/webapp/redux/reducers/general.js +++ b/webapp/redux/reducers/general.js @@ -21,6 +21,33 @@ export const GENERAL_DEFAULT_STATE = { instances: [], }; +const applySelection = (data, selectedInstances) => { + const smap = new Map(selectedInstances.map((i) => [i, true])); + const newData = data.map((item) => ({ + ...item, + selected: false, + })); + const dmap = new Map(newData.map((i) => [i.instancePath, true])); + + smap.forEach((value, key) => { + const item = dmap.get(key); + if (!item) { + newData.push({ + instancePath: key, + color: undefined, + selected: true, + }); + } + }); + const canvasData = newData.filter((item) => { + if ((item?.selected !== undefined && item?.selected === false) && item?.color === undefined) { + return false; + } + return true; + }); + return canvasData; +}; + // reducer function export default function reduceGeneral (state = GENERAL_DEFAULT_STATE, action) { switch (action.type) { @@ -76,6 +103,10 @@ export default function reduceGeneral (state = GENERAL_DEFAULT_STATE, action) { case Actions.REMOVE_CANVAS_INSTANCES: { return { ...state }; } + case Actions.SELECT_INSTANCE: { + const newData = applySelection(action.data.instance, action.data.selectedInstances); + return { ...state, instances: [...newData] }; + } default: { return state; }