diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala deleted file mode 100644 index 8488984..0000000 --- a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.generators - -import scala.util.Random - -class RandomSequenceGenerator(N: Int) extends SequenceGenerator { - /** - * Generate simple alphanumeric string of size N - * - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 0): String = { - Random.alphanumeric.take(trials).mkString("") - } -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala deleted file mode 100644 index 8488984..0000000 --- a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.generators - -import scala.util.Random - -class RandomSequenceGenerator(N: Int) extends SequenceGenerator { - /** - * Generate simple alphanumeric string of size N - * - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 0): String = { - Random.alphanumeric.take(trials).mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala deleted file mode 100644 index 9cb0604..0000000 --- a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.xcit.nback.generators - -/** - * Companion object and buiilders interface for Strategy pattern of the sequence generators. - * Params: - * "n": The "n" property in n-back (N items back must match the current stimuli). - * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement - * learning methods. - * - */ -object SequenceGenerator { - def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) - def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) - def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) -} - -/** - * Generic n-back class to generate a sequence of n-back items with HMM. - */ -trait SequenceGenerator { - - /** - * Generate simple alphanumeric string of size N - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - def generate(trials: Int = 0): String -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala deleted file mode 100644 index 8488984..0000000 --- a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.generators - -import scala.util.Random - -class RandomSequenceGenerator(N: Int) extends SequenceGenerator { - /** - * Generate simple alphanumeric string of size N - * - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 0): String = { - Random.alphanumeric.take(trials).mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala deleted file mode 100644 index 9cb0604..0000000 --- a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.xcit.nback.generators - -/** - * Companion object and buiilders interface for Strategy pattern of the sequence generators. - * Params: - * "n": The "n" property in n-back (N items back must match the current stimuli). - * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement - * learning methods. - * - */ -object SequenceGenerator { - def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) - def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) - def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) -} - -/** - * Generic n-back class to generate a sequence of n-back items with HMM. - */ -trait SequenceGenerator { - - /** - * Generate simple alphanumeric string of size N - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - def generate(trials: Int = 0): String -} diff --git a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala deleted file mode 100644 index 3549506..0000000 --- a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala +++ /dev/null @@ -1,89 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable.ListBuffer -import scala.util.Random - -/** - * Reference: Ralph2014 - Appendix A - * - * @param alphabet Alphabet used to generate the sequence, excluding the target item - * @param target Target item. It must not be included in [[alphabet]]. - * @param N The set size of stimuli to remember (n in n-back). Default is 3. - * @param T Number of target trials - * @param D Number of non-matching distractor trials. - * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). - * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). - */ -class SkewedSequenceGenerator( - alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 - target: Char = 'A', - N: Int = 3, - T: Int = 24, - D: Int = 32, - L1: Int = 4, - L2: Int = 4) - extends SequenceGenerator { - - - val trials = T + D + L1 + L2 - - /** - * Types of each trial (L1, L2, T, or D). - */ - private object TrialType extends Enumeration { - val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value - } - - /** - * Ignores trials. Trials' value is defined by N+T+D+L1+L2 - * @param t number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(t: Int): String = { - // 1. Init buffer with empty list of items :-). Why am I explaining this? - var buffer = ListBuffer[String]() - - var trial = 0 - - while (trial < trials) { - trial += 1 - - // 2. For each trial, generate a random type (L1, L2, T, or D) - // 3. Based on the generated type, generate the item and append it to the buffer - buffer += {nextTrialType(trials-trial) match { - case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => - buffer(buffer.length - (N + 2)) - case TrialType.TARGET if buffer.length > N => - buffer(buffer.length - (N - 1)) - case TrialType.LURE_BEFORE_TARGET if buffer.length > N => - buffer(buffer.length - N) - case _ => - alphabet.charAt(Random.nextInt(alphabet.length)).toString - }} - - } - // 4. Convert buffer to string and return it as result - buffer.mkString("") - } - - /** - * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). - * Random range: [1, remaining_trials] (both are inclusive) - * @return a trial type to be used to generate an item for current trial - */ - private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { - case rnd if rnd <= L1 => - L1 -= 1 - TrialType.LURE_BEFORE_TARGET - case rnd if rnd > L1 && rnd <= L1 + L2 => - L2 -= 1 - TrialType.LURE_AFTER_TARGET - case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => - T -= 1 - TrialType.TARGET - case _ => - D -= 1 - TrialType.DISTRACTOR - - } -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala deleted file mode 100644 index 8488984..0000000 --- a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.generators - -import scala.util.Random - -class RandomSequenceGenerator(N: Int) extends SequenceGenerator { - /** - * Generate simple alphanumeric string of size N - * - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 0): String = { - Random.alphanumeric.take(trials).mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala deleted file mode 100644 index 9cb0604..0000000 --- a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.xcit.nback.generators - -/** - * Companion object and buiilders interface for Strategy pattern of the sequence generators. - * Params: - * "n": The "n" property in n-back (N items back must match the current stimuli). - * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement - * learning methods. - * - */ -object SequenceGenerator { - def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) - def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) - def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) -} - -/** - * Generic n-back class to generate a sequence of n-back items with HMM. - */ -trait SequenceGenerator { - - /** - * Generate simple alphanumeric string of size N - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - def generate(trials: Int = 0): String -} diff --git a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala deleted file mode 100644 index 3549506..0000000 --- a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala +++ /dev/null @@ -1,89 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable.ListBuffer -import scala.util.Random - -/** - * Reference: Ralph2014 - Appendix A - * - * @param alphabet Alphabet used to generate the sequence, excluding the target item - * @param target Target item. It must not be included in [[alphabet]]. - * @param N The set size of stimuli to remember (n in n-back). Default is 3. - * @param T Number of target trials - * @param D Number of non-matching distractor trials. - * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). - * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). - */ -class SkewedSequenceGenerator( - alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 - target: Char = 'A', - N: Int = 3, - T: Int = 24, - D: Int = 32, - L1: Int = 4, - L2: Int = 4) - extends SequenceGenerator { - - - val trials = T + D + L1 + L2 - - /** - * Types of each trial (L1, L2, T, or D). - */ - private object TrialType extends Enumeration { - val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value - } - - /** - * Ignores trials. Trials' value is defined by N+T+D+L1+L2 - * @param t number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(t: Int): String = { - // 1. Init buffer with empty list of items :-). Why am I explaining this? - var buffer = ListBuffer[String]() - - var trial = 0 - - while (trial < trials) { - trial += 1 - - // 2. For each trial, generate a random type (L1, L2, T, or D) - // 3. Based on the generated type, generate the item and append it to the buffer - buffer += {nextTrialType(trials-trial) match { - case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => - buffer(buffer.length - (N + 2)) - case TrialType.TARGET if buffer.length > N => - buffer(buffer.length - (N - 1)) - case TrialType.LURE_BEFORE_TARGET if buffer.length > N => - buffer(buffer.length - N) - case _ => - alphabet.charAt(Random.nextInt(alphabet.length)).toString - }} - - } - // 4. Convert buffer to string and return it as result - buffer.mkString("") - } - - /** - * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). - * Random range: [1, remaining_trials] (both are inclusive) - * @return a trial type to be used to generate an item for current trial - */ - private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { - case rnd if rnd <= L1 => - L1 -= 1 - TrialType.LURE_BEFORE_TARGET - case rnd if rnd > L1 && rnd <= L1 + L2 => - L2 -= 1 - TrialType.LURE_AFTER_TARGET - case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => - T -= 1 - TrialType.TARGET - case _ => - D -= 1 - TrialType.DISTRACTOR - - } -} diff --git a/src/main/scala/org/xcit/nback/markov/Main.scala b/src/main/scala/org/xcit/nback/markov/Main.scala deleted file mode 100644 index fdcb58b..0000000 --- a/src/main/scala/org/xcit/nback/markov/Main.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.xcit.nback.markov - -/** - * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. - */ -class Main extends App { - //TODO create a MarkovChain object or use the singleton - //TODO train to set transitions - //TODO benchmark -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala deleted file mode 100644 index 8488984..0000000 --- a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.generators - -import scala.util.Random - -class RandomSequenceGenerator(N: Int) extends SequenceGenerator { - /** - * Generate simple alphanumeric string of size N - * - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 0): String = { - Random.alphanumeric.take(trials).mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala deleted file mode 100644 index 9cb0604..0000000 --- a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.xcit.nback.generators - -/** - * Companion object and buiilders interface for Strategy pattern of the sequence generators. - * Params: - * "n": The "n" property in n-back (N items back must match the current stimuli). - * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement - * learning methods. - * - */ -object SequenceGenerator { - def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) - def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) - def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) -} - -/** - * Generic n-back class to generate a sequence of n-back items with HMM. - */ -trait SequenceGenerator { - - /** - * Generate simple alphanumeric string of size N - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - def generate(trials: Int = 0): String -} diff --git a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala deleted file mode 100644 index 3549506..0000000 --- a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala +++ /dev/null @@ -1,89 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable.ListBuffer -import scala.util.Random - -/** - * Reference: Ralph2014 - Appendix A - * - * @param alphabet Alphabet used to generate the sequence, excluding the target item - * @param target Target item. It must not be included in [[alphabet]]. - * @param N The set size of stimuli to remember (n in n-back). Default is 3. - * @param T Number of target trials - * @param D Number of non-matching distractor trials. - * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). - * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). - */ -class SkewedSequenceGenerator( - alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 - target: Char = 'A', - N: Int = 3, - T: Int = 24, - D: Int = 32, - L1: Int = 4, - L2: Int = 4) - extends SequenceGenerator { - - - val trials = T + D + L1 + L2 - - /** - * Types of each trial (L1, L2, T, or D). - */ - private object TrialType extends Enumeration { - val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value - } - - /** - * Ignores trials. Trials' value is defined by N+T+D+L1+L2 - * @param t number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(t: Int): String = { - // 1. Init buffer with empty list of items :-). Why am I explaining this? - var buffer = ListBuffer[String]() - - var trial = 0 - - while (trial < trials) { - trial += 1 - - // 2. For each trial, generate a random type (L1, L2, T, or D) - // 3. Based on the generated type, generate the item and append it to the buffer - buffer += {nextTrialType(trials-trial) match { - case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => - buffer(buffer.length - (N + 2)) - case TrialType.TARGET if buffer.length > N => - buffer(buffer.length - (N - 1)) - case TrialType.LURE_BEFORE_TARGET if buffer.length > N => - buffer(buffer.length - N) - case _ => - alphabet.charAt(Random.nextInt(alphabet.length)).toString - }} - - } - // 4. Convert buffer to string and return it as result - buffer.mkString("") - } - - /** - * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). - * Random range: [1, remaining_trials] (both are inclusive) - * @return a trial type to be used to generate an item for current trial - */ - private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { - case rnd if rnd <= L1 => - L1 -= 1 - TrialType.LURE_BEFORE_TARGET - case rnd if rnd > L1 && rnd <= L1 + L2 => - L2 -= 1 - TrialType.LURE_AFTER_TARGET - case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => - T -= 1 - TrialType.TARGET - case _ => - D -= 1 - TrialType.DISTRACTOR - - } -} diff --git a/src/main/scala/org/xcit/nback/markov/Main.scala b/src/main/scala/org/xcit/nback/markov/Main.scala deleted file mode 100644 index fdcb58b..0000000 --- a/src/main/scala/org/xcit/nback/markov/Main.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.xcit.nback.markov - -/** - * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. - */ -class Main extends App { - //TODO create a MarkovChain object or use the singleton - //TODO train to set transitions - //TODO benchmark -} diff --git a/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala deleted file mode 100644 index 5694371..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala +++ /dev/null @@ -1,39 +0,0 @@ -package org.xcit.nback.markov.models - -import scala.collection.mutable - -/** - * Represents a simple markov chain - * TODO: must be extended to HMM (add emission probability) - */ -class MarkovChain(startState: State) { - val states: mutable.MutableList[State] = mutable.MutableList[State]() - - var currentState: State = startState - - //TODO - def next(): State = ??? - - //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble - - def totalTransitions(): Int = { - var total = 0 - states.foreach(total += _.transitions.size) - total - } - - /** - * Add a transition from "from" state to "to" state with a defined probability. - * @param from origin node - * @param to destination node - * @param probability transition probability - */ - def addTransition(from: State, to: State, probability: Double): Unit = - states - .find(_.label == from.label) - .map(s => - s.transitions += Transition(to, probability) - ) - - -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala deleted file mode 100644 index 8488984..0000000 --- a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.generators - -import scala.util.Random - -class RandomSequenceGenerator(N: Int) extends SequenceGenerator { - /** - * Generate simple alphanumeric string of size N - * - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 0): String = { - Random.alphanumeric.take(trials).mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala deleted file mode 100644 index 9cb0604..0000000 --- a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.xcit.nback.generators - -/** - * Companion object and buiilders interface for Strategy pattern of the sequence generators. - * Params: - * "n": The "n" property in n-back (N items back must match the current stimuli). - * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement - * learning methods. - * - */ -object SequenceGenerator { - def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) - def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) - def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) -} - -/** - * Generic n-back class to generate a sequence of n-back items with HMM. - */ -trait SequenceGenerator { - - /** - * Generate simple alphanumeric string of size N - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - def generate(trials: Int = 0): String -} diff --git a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala deleted file mode 100644 index 3549506..0000000 --- a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala +++ /dev/null @@ -1,89 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable.ListBuffer -import scala.util.Random - -/** - * Reference: Ralph2014 - Appendix A - * - * @param alphabet Alphabet used to generate the sequence, excluding the target item - * @param target Target item. It must not be included in [[alphabet]]. - * @param N The set size of stimuli to remember (n in n-back). Default is 3. - * @param T Number of target trials - * @param D Number of non-matching distractor trials. - * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). - * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). - */ -class SkewedSequenceGenerator( - alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 - target: Char = 'A', - N: Int = 3, - T: Int = 24, - D: Int = 32, - L1: Int = 4, - L2: Int = 4) - extends SequenceGenerator { - - - val trials = T + D + L1 + L2 - - /** - * Types of each trial (L1, L2, T, or D). - */ - private object TrialType extends Enumeration { - val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value - } - - /** - * Ignores trials. Trials' value is defined by N+T+D+L1+L2 - * @param t number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(t: Int): String = { - // 1. Init buffer with empty list of items :-). Why am I explaining this? - var buffer = ListBuffer[String]() - - var trial = 0 - - while (trial < trials) { - trial += 1 - - // 2. For each trial, generate a random type (L1, L2, T, or D) - // 3. Based on the generated type, generate the item and append it to the buffer - buffer += {nextTrialType(trials-trial) match { - case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => - buffer(buffer.length - (N + 2)) - case TrialType.TARGET if buffer.length > N => - buffer(buffer.length - (N - 1)) - case TrialType.LURE_BEFORE_TARGET if buffer.length > N => - buffer(buffer.length - N) - case _ => - alphabet.charAt(Random.nextInt(alphabet.length)).toString - }} - - } - // 4. Convert buffer to string and return it as result - buffer.mkString("") - } - - /** - * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). - * Random range: [1, remaining_trials] (both are inclusive) - * @return a trial type to be used to generate an item for current trial - */ - private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { - case rnd if rnd <= L1 => - L1 -= 1 - TrialType.LURE_BEFORE_TARGET - case rnd if rnd > L1 && rnd <= L1 + L2 => - L2 -= 1 - TrialType.LURE_AFTER_TARGET - case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => - T -= 1 - TrialType.TARGET - case _ => - D -= 1 - TrialType.DISTRACTOR - - } -} diff --git a/src/main/scala/org/xcit/nback/markov/Main.scala b/src/main/scala/org/xcit/nback/markov/Main.scala deleted file mode 100644 index fdcb58b..0000000 --- a/src/main/scala/org/xcit/nback/markov/Main.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.xcit.nback.markov - -/** - * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. - */ -class Main extends App { - //TODO create a MarkovChain object or use the singleton - //TODO train to set transitions - //TODO benchmark -} diff --git a/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala deleted file mode 100644 index 5694371..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala +++ /dev/null @@ -1,39 +0,0 @@ -package org.xcit.nback.markov.models - -import scala.collection.mutable - -/** - * Represents a simple markov chain - * TODO: must be extended to HMM (add emission probability) - */ -class MarkovChain(startState: State) { - val states: mutable.MutableList[State] = mutable.MutableList[State]() - - var currentState: State = startState - - //TODO - def next(): State = ??? - - //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble - - def totalTransitions(): Int = { - var total = 0 - states.foreach(total += _.transitions.size) - total - } - - /** - * Add a transition from "from" state to "to" state with a defined probability. - * @param from origin node - * @param to destination node - * @param probability transition probability - */ - def addTransition(from: State, to: State, probability: Double): Unit = - states - .find(_.label == from.label) - .map(s => - s.transitions += Transition(to, probability) - ) - - -} diff --git a/src/main/scala/org/xcit/nback/markov/models/State.scala b/src/main/scala/org/xcit/nback/markov/models/State.scala deleted file mode 100644 index fbbd844..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/State.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.markov.models - -import scala.collection.mutable - -/** - * A Single state with a label in Markov chain - * @param label A simple string label, representing the state and node - */ -class State(val label: String) { - - /** - * Keep track of transitions from this state to other states. - */ - val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala deleted file mode 100644 index 8488984..0000000 --- a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.generators - -import scala.util.Random - -class RandomSequenceGenerator(N: Int) extends SequenceGenerator { - /** - * Generate simple alphanumeric string of size N - * - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 0): String = { - Random.alphanumeric.take(trials).mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala deleted file mode 100644 index 9cb0604..0000000 --- a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.xcit.nback.generators - -/** - * Companion object and buiilders interface for Strategy pattern of the sequence generators. - * Params: - * "n": The "n" property in n-back (N items back must match the current stimuli). - * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement - * learning methods. - * - */ -object SequenceGenerator { - def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) - def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) - def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) -} - -/** - * Generic n-back class to generate a sequence of n-back items with HMM. - */ -trait SequenceGenerator { - - /** - * Generate simple alphanumeric string of size N - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - def generate(trials: Int = 0): String -} diff --git a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala deleted file mode 100644 index 3549506..0000000 --- a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala +++ /dev/null @@ -1,89 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable.ListBuffer -import scala.util.Random - -/** - * Reference: Ralph2014 - Appendix A - * - * @param alphabet Alphabet used to generate the sequence, excluding the target item - * @param target Target item. It must not be included in [[alphabet]]. - * @param N The set size of stimuli to remember (n in n-back). Default is 3. - * @param T Number of target trials - * @param D Number of non-matching distractor trials. - * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). - * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). - */ -class SkewedSequenceGenerator( - alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 - target: Char = 'A', - N: Int = 3, - T: Int = 24, - D: Int = 32, - L1: Int = 4, - L2: Int = 4) - extends SequenceGenerator { - - - val trials = T + D + L1 + L2 - - /** - * Types of each trial (L1, L2, T, or D). - */ - private object TrialType extends Enumeration { - val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value - } - - /** - * Ignores trials. Trials' value is defined by N+T+D+L1+L2 - * @param t number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(t: Int): String = { - // 1. Init buffer with empty list of items :-). Why am I explaining this? - var buffer = ListBuffer[String]() - - var trial = 0 - - while (trial < trials) { - trial += 1 - - // 2. For each trial, generate a random type (L1, L2, T, or D) - // 3. Based on the generated type, generate the item and append it to the buffer - buffer += {nextTrialType(trials-trial) match { - case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => - buffer(buffer.length - (N + 2)) - case TrialType.TARGET if buffer.length > N => - buffer(buffer.length - (N - 1)) - case TrialType.LURE_BEFORE_TARGET if buffer.length > N => - buffer(buffer.length - N) - case _ => - alphabet.charAt(Random.nextInt(alphabet.length)).toString - }} - - } - // 4. Convert buffer to string and return it as result - buffer.mkString("") - } - - /** - * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). - * Random range: [1, remaining_trials] (both are inclusive) - * @return a trial type to be used to generate an item for current trial - */ - private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { - case rnd if rnd <= L1 => - L1 -= 1 - TrialType.LURE_BEFORE_TARGET - case rnd if rnd > L1 && rnd <= L1 + L2 => - L2 -= 1 - TrialType.LURE_AFTER_TARGET - case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => - T -= 1 - TrialType.TARGET - case _ => - D -= 1 - TrialType.DISTRACTOR - - } -} diff --git a/src/main/scala/org/xcit/nback/markov/Main.scala b/src/main/scala/org/xcit/nback/markov/Main.scala deleted file mode 100644 index fdcb58b..0000000 --- a/src/main/scala/org/xcit/nback/markov/Main.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.xcit.nback.markov - -/** - * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. - */ -class Main extends App { - //TODO create a MarkovChain object or use the singleton - //TODO train to set transitions - //TODO benchmark -} diff --git a/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala deleted file mode 100644 index 5694371..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala +++ /dev/null @@ -1,39 +0,0 @@ -package org.xcit.nback.markov.models - -import scala.collection.mutable - -/** - * Represents a simple markov chain - * TODO: must be extended to HMM (add emission probability) - */ -class MarkovChain(startState: State) { - val states: mutable.MutableList[State] = mutable.MutableList[State]() - - var currentState: State = startState - - //TODO - def next(): State = ??? - - //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble - - def totalTransitions(): Int = { - var total = 0 - states.foreach(total += _.transitions.size) - total - } - - /** - * Add a transition from "from" state to "to" state with a defined probability. - * @param from origin node - * @param to destination node - * @param probability transition probability - */ - def addTransition(from: State, to: State, probability: Double): Unit = - states - .find(_.label == from.label) - .map(s => - s.transitions += Transition(to, probability) - ) - - -} diff --git a/src/main/scala/org/xcit/nback/markov/models/State.scala b/src/main/scala/org/xcit/nback/markov/models/State.scala deleted file mode 100644 index fbbd844..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/State.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.markov.models - -import scala.collection.mutable - -/** - * A Single state with a label in Markov chain - * @param label A simple string label, representing the state and node - */ -class State(val label: String) { - - /** - * Keep track of transitions from this state to other states. - */ - val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() -} diff --git a/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/src/main/scala/org/xcit/nback/markov/models/Transition.scala deleted file mode 100644 index bc20a9e..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/Transition.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.xcit.nback.markov.models - -/** - * A Single transition in Markov chain graph. It is stored in "from" node. - * @param to the ending node of the edge - * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). - */ -case class Transition(to: State, probability: Double = 0.0) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5345e9e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f776680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__": true + }, + "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..23d2206 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = 'pypi' +url = 'https://pypi.python.org/simple' +verify_ssl = true + +[dev-packages] +pylint = "*" + +[packages] +requests = "*" +python-constraint = "*" +Numberjack = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..25ca7bf --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,149 @@ +{ + "_meta": { + "hash": { + "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "numberjack": { + "hashes": [ + "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "python-constraint": { + "hashes": [ + "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md index 623d3e4..1e7491b 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,10 @@ # N-Back Sequence Generators -**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! +This project contains Python implementations of various sequence generators for the n-back task. -**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. +## Build and Run +## Skewed Random Generator -## Compile/Build +## Even Random Generator -All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. - -To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). - -## Run - -To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: - -`sbt run` - -## Benchmarks - -To run a single benchmark use the following command: - -`sbt testOnly ` - -To run all benchmarks and generate respective reports run the following command: - -`sbt test` - - -`//TODO add list of benchmkarks` - - -## Documentations - -Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). - -You can comment there or inside codes if you have any idea :-). diff --git a/build.sbt b/build.sbt deleted file mode 100644 index f92a6be..0000000 --- a/build.sbt +++ /dev/null @@ -1,10 +0,0 @@ -name := "nback.markov" -organization := "org.xcit" - -version := "0.1" - -scalaVersion := "2.12.7" - - -//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/even_random_generator.py b/even_random_generator.py new file mode 100644 index 0000000..fda40d8 --- /dev/null +++ b/even_random_generator.py @@ -0,0 +1,75 @@ +from constraint import * + +import itertools as it + +import Numberjack as nj + +class EvenRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials=64, tl=1): + self.tl, self.trials, self.choices = tl, trials, choices + + def generate(self): + seqs = self._generate_initial_sequences() + return self._find_optimal_sequence(seqs, 0.2) + + def _generate_initial_sequences(self): + """ + Generates initial sequence of items based on choices and number of desired trials. + In EvenRandom sequences, all stimuli have same number of appearances. + """ + + pool = it.product(self.choices, repeat=self.trials) + return pool + + def _find_optimal_sequence_numberjack(self,tl_ratio): + """Optimize with Numberjack""" + + cost = nj.Variable(0, 100, 'cost') + seqs = nj.Variable([]) # all sequences + + model = nj.Model( + seqs., + cost == self.calculate_tl_ratio(seq) - tl_ratio, + nj.Minimise(cost) + ) + solver = model.load('Mistral') + if solver.solve(): + solver.printStatistics() + else: + print("No solution with Numberjack") + + + def _find_optimal_sequence(self, sequences, tl_ratio): + """Optimize a sequence to match a desired tl ratio with python-constraints""" + + p = Problem() + + # TODO add all possible values for seq (its domain) + p.addVariable("seq", list(sequences)) + + p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) + + return p.getSolution() + + @staticmethod + def calculate_tl_ratio(seq): + """Calculates the T:L ratio of a sequence.""" + targets = 0 + lures = 0 + for index, item in seq: + if item == seq[index-2]: + targets += 1 + elif item == seq[index-1] or item == seq[index-3]: + lures += 1 + # avoid division by zero + if lures == 0: + lures = 1 + return targets/lures + + +if __name__ == '__main__': + generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) + generated_seq = generator.generate() + print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/ga_optimized_generator.py b/ga_optimized_generator.py new file mode 100644 index 0000000..56b7cc0 --- /dev/null +++ b/ga_optimized_generator.py @@ -0,0 +1,141 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __append_chunk(self, prefix="", chunk_size=8): + chunk_generation = 0 + pool = [] + + def __init_pool(self, pool_size, chunk_size = 8) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + population = it.combinations_with_replacement(self.choices, chunk_size) + sample = random.sample(list(population), pool_size) + self.pool = list(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, pool: list, count=1) -> list: + """ + Find best available sequences from the current pool based on the cost function. + :param count: Number of desired best sequences to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(pool, key=lambda _: self.cost(_)) + return sorted_pool[:count] + + def distribution_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += 1.0 if costs.__contains__(c) else 0.0 + + # TODO instead of normalizing all, only normalize the max value + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k, v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness (or cost) of a sequence. + It's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + # TODO merge different cost functions with weights + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.__distribution_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +# Demo +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.distribution_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/iterative_ga_optimized_generator.py b/iterative_ga_optimized_generator.py new file mode 100644 index 0000000..4f2ef4c --- /dev/null +++ b/iterative_ga_optimized_generator.py @@ -0,0 +1,134 @@ +import itertools as it +import random + + +class GAOptimizedRandomGenerator: + """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" + + def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): + """Initialize the genetic algorithm optimizer for n-back sequences. + :param choices: + :param trials: + :param tl: + :param pool_size: + :param n: + """ + self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n + self.pool = [] + self.__init_pool(pool_size) + + def generate(self): + """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be + close to the desired ones but not exactly the same. + :return: a sequence of items in "list" format. + """ + generation = 0 + best_parent = self.__find_best_parents(1)[0] + while self.cost(best_parent) > 0.1 and generation < 1000: + generation += 1 + if random.random() > 0.5: + self.pool = list(map(lambda s: self.mutate(s), self.pool)) + self.pool = self.crossover_all() + best_parent = self.__find_best_parents(1)[0] + print(best_parent, 'cost=%f' % self.cost(best_parent)) + return best_parent + + def __init_pool(self, pool_size) -> list: + """ + Initialize solution pool. + :param pool_size: Num of initial random solutions + :return: initial pool of + """ + print("Initializing the pool...") + self.pool.clear() + all_comb = it.combinations_with_replacement(self.choices, self.trials) + sample = random.sample(list(all_comb), pool_size) + self.pool.extend(map(lambda _: ''.join(_), sample)) + return self.pool + + def __find_best_parents(self, count=1): + """ + Find best gene(s) or parent(s) from the current pool. + :param count: Number of desired best parents to be returned. Default is 1. + :return: A list of most fit sequences. + """ + sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) + return sorted_pool[:count] + + def even_dist_cost(self, seq): + """ + Calculate fitness according to the similarity to the desired uniform distribution. + :param seq: a string + :return: + """ + costs = {c: 0.0 for c in self.choices} + for c in list(seq): + costs[c] += (1.0 if costs.__contains__(c) else 0.0) + costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} + return max(list(costs.values())) + + def cost(self, seq): + """ + Calculate overall fitness of a sequence (block of trials). + Right now it's a cost function, so we try to minimize this cost. + :param seq: + :return: + """ + # add fitness for uniform distribution of all stimuli + return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) + + def crossover_all(self): + """ + Perform random crossover for all pairs. + :return: new pool + """ + new_pool = [] + for i in range(int(self.pool_size/2)): + seq1 = self.pool[i*2] # change to weighted random + seq2 = self.pool[i*2 + 1] # change to weighted random + new_pool.extend(self.crossover(seq1, seq2)) + + return new_pool + + def crossover(self, seq1, seq2): + """ + Crossover two sequences. + :param seq1: + :param seq2: + :return: + """ + pos = random.randint(0, self.trials) + return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] + + def mutate(self, seq): + if random.random() > 0.5: + pos = random.randint(0, len(seq)-1) + seq_list = list(seq) + seq_list[pos] = random.choice(self.choices) + return ''.join(seq_list) + return seq + + @staticmethod + def calculate_tl_ratio(seq, n: int): + """Calculates the T/L ratio in a block of trials.""" + targets = 0.0 + lures = 0.0 + for index in range(n, len(seq)): + item = seq[index] + if item == seq[index - n]: + targets += 1.0 + elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: + lures += 1.0 + if lures - 0.0 < 0.001: # avoid division by zero + lures = 0.001 + return targets/lures + + +if __name__ == '__main__': + + generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) + sq = generator.generate() + tl_ratio = generator.calculate_tl_ratio(sq, n=2) + even_dist = generator.even_dist_cost(sq) + + print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/main.py b/main.py new file mode 100644 index 0000000..84f16d3 --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +import requests + +response = requests.get('https://httpbin.org/ip') + +print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/markov/README.md b/markov/README.md new file mode 100644 index 0000000..623d3e4 --- /dev/null +++ b/markov/README.md @@ -0,0 +1,38 @@ +# N-Back Sequence Generators + +**Note #1:** It's a work in progress (WIP), and commits will be tagged with WIP if it's not expected to work! + +**Note #2:** All the following commands can be run inside `sbt` shell. For the sake of simplicity, it is assumed the developer is in her terminal when running the commands. + + +## Compile/Build + +All python classes include scripts to manage executing a sample code. Just use `python ` to the sample codes of a generator. + +To build Scala codes and compile them use `sbt compile` and `sbt build`. It's recommended to use `sbt clean` before running any of these commands :-). + +## Run + +To run default generator (with random sequence generator), and some other benchmarking and reporting outputs use the following command: + +`sbt run` + +## Benchmarks + +To run a single benchmark use the following command: + +`sbt testOnly ` + +To run all benchmarks and generate respective reports run the following command: + +`sbt test` + + +`//TODO add list of benchmkarks` + + +## Documentations + +Currently all respective documents and UML diagrams are available at our [Google Drive as document file](https://docs.google.com/document/d/1tASkipaysX7vnNUcFYJlWsJJTrBcSZteLFQZUPY4_6c/edit?usp=sharing). + +You can comment there or inside codes if you have any idea :-). diff --git a/markov/build.sbt b/markov/build.sbt new file mode 100644 index 0000000..f92a6be --- /dev/null +++ b/markov/build.sbt @@ -0,0 +1,10 @@ +name := "nback.markov" +organization := "org.xcit" + +version := "0.1" + +scalaVersion := "2.12.7" + + +//libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.+" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.+" % "test" diff --git a/markov/project/build.properties b/markov/project/build.properties new file mode 100644 index 0000000..e71780d --- /dev/null +++ b/markov/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.2.6 \ No newline at end of file diff --git a/markov/project/plugins.sbt b/markov/project/plugins.sbt new file mode 100644 index 0000000..0f102c4 --- /dev/null +++ b/markov/project/plugins.sbt @@ -0,0 +1 @@ +//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala new file mode 100644 index 0000000..021ea42 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala @@ -0,0 +1,34 @@ +package org.xcit.nback.generators + +import scala.collection.mutable +import scala.util.Random + +/** + * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. + * Reference: Ralph2014 + * + * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar + * characters). Default alphabet contains 20 characters. + * @param target target item (default is 'A'). + */ +class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { + /** + * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 64): String = { + var results = mutable.ListBuffer[String]() + val items = List.range(0, trials) + + // decide if target or non-target with 50/50 chance. + //TODO change probability according to a new class param + if (Random.nextBoolean) + results += target.toString + else + items.foreach( + results += alphabet.charAt(Random.nextInt(alphabet.length)).toString + ) + results.mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala new file mode 100644 index 0000000..d10e633 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala @@ -0,0 +1,6 @@ +package org.xcit.nback.generators + +//TODO +class MarkovChainSequenceGenerator { + +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala new file mode 100644 index 0000000..8488984 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.generators + +import scala.util.Random + +class RandomSequenceGenerator(N: Int) extends SequenceGenerator { + /** + * Generate simple alphanumeric string of size N + * + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(trials: Int = 0): String = { + Random.alphanumeric.take(trials).mkString("") + } +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala new file mode 100644 index 0000000..9cb0604 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala @@ -0,0 +1,28 @@ +package org.xcit.nback.generators + +/** + * Companion object and buiilders interface for Strategy pattern of the sequence generators. + * Params: + * "n": The "n" property in n-back (N items back must match the current stimuli). + * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement + * learning methods. + * + */ +object SequenceGenerator { + def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) + def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) + def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) +} + +/** + * Generic n-back class to generate a sequence of n-back items with HMM. + */ +trait SequenceGenerator { + + /** + * Generate simple alphanumeric string of size N + * @param trials number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + def generate(trials: Int = 0): String +} diff --git a/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala new file mode 100644 index 0000000..3549506 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala @@ -0,0 +1,89 @@ +package org.xcit.nback.generators + +import scala.collection.mutable.ListBuffer +import scala.util.Random + +/** + * Reference: Ralph2014 - Appendix A + * + * @param alphabet Alphabet used to generate the sequence, excluding the target item + * @param target Target item. It must not be included in [[alphabet]]. + * @param N The set size of stimuli to remember (n in n-back). Default is 3. + * @param T Number of target trials + * @param D Number of non-matching distractor trials. + * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). + * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). + */ +class SkewedSequenceGenerator( + alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 + target: Char = 'A', + N: Int = 3, + T: Int = 24, + D: Int = 32, + L1: Int = 4, + L2: Int = 4) + extends SequenceGenerator { + + + val trials = T + D + L1 + L2 + + /** + * Types of each trial (L1, L2, T, or D). + */ + private object TrialType extends Enumeration { + val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value + } + + /** + * Ignores trials. Trials' value is defined by N+T+D+L1+L2 + * @param t number of items in n-back sequence. + * @return string of size "trials" with random alphanumeric characters. + */ + override def generate(t: Int): String = { + // 1. Init buffer with empty list of items :-). Why am I explaining this? + var buffer = ListBuffer[String]() + + var trial = 0 + + while (trial < trials) { + trial += 1 + + // 2. For each trial, generate a random type (L1, L2, T, or D) + // 3. Based on the generated type, generate the item and append it to the buffer + buffer += {nextTrialType(trials-trial) match { + case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => + buffer(buffer.length - (N + 2)) + case TrialType.TARGET if buffer.length > N => + buffer(buffer.length - (N - 1)) + case TrialType.LURE_BEFORE_TARGET if buffer.length > N => + buffer(buffer.length - N) + case _ => + alphabet.charAt(Random.nextInt(alphabet.length)).toString + }} + + } + // 4. Convert buffer to string and return it as result + buffer.mkString("") + } + + /** + * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). + * Random range: [1, remaining_trials] (both are inclusive) + * @return a trial type to be used to generate an item for current trial + */ + private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { + case rnd if rnd <= L1 => + L1 -= 1 + TrialType.LURE_BEFORE_TARGET + case rnd if rnd > L1 && rnd <= L1 + L2 => + L2 -= 1 + TrialType.LURE_AFTER_TARGET + case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => + T -= 1 + TrialType.TARGET + case _ => + D -= 1 + TrialType.DISTRACTOR + + } +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/Main.scala b/markov/src/main/scala/org/xcit/nback/markov/Main.scala new file mode 100644 index 0000000..fdcb58b --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/Main.scala @@ -0,0 +1,10 @@ +package org.xcit.nback.markov + +/** + * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. + */ +class Main extends App { + //TODO create a MarkovChain object or use the singleton + //TODO train to set transitions + //TODO benchmark +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala new file mode 100644 index 0000000..5694371 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala @@ -0,0 +1,39 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * Represents a simple markov chain + * TODO: must be extended to HMM (add emission probability) + */ +class MarkovChain(startState: State) { + val states: mutable.MutableList[State] = mutable.MutableList[State]() + + var currentState: State = startState + + //TODO + def next(): State = ??? + + //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble + + def totalTransitions(): Int = { + var total = 0 + states.foreach(total += _.transitions.size) + total + } + + /** + * Add a transition from "from" state to "to" state with a defined probability. + * @param from origin node + * @param to destination node + * @param probability transition probability + */ + def addTransition(from: State, to: State, probability: Double): Unit = + states + .find(_.label == from.label) + .map(s => + s.transitions += Transition(to, probability) + ) + + +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/State.scala b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala new file mode 100644 index 0000000..fbbd844 --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/State.scala @@ -0,0 +1,15 @@ +package org.xcit.nback.markov.models + +import scala.collection.mutable + +/** + * A Single state with a label in Markov chain + * @param label A simple string label, representing the state and node + */ +class State(val label: String) { + + /** + * Keep track of transitions from this state to other states. + */ + val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() +} diff --git a/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala new file mode 100644 index 0000000..bc20a9e --- /dev/null +++ b/markov/src/main/scala/org/xcit/nback/markov/models/Transition.scala @@ -0,0 +1,8 @@ +package org.xcit.nback.markov.models + +/** + * A Single transition in Markov chain graph. It is stored in "from" node. + * @param to the ending node of the edge + * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). + */ +case class Transition(to: State, probability: Double = 0.0) diff --git a/markov/src/test/scala/ThreeBackBenchmark.scala b/markov/src/test/scala/ThreeBackBenchmark.scala new file mode 100644 index 0000000..0c7dad9 --- /dev/null +++ b/markov/src/test/scala/ThreeBackBenchmark.scala @@ -0,0 +1,7 @@ + +/** + * TODO Benchmark 3-back simple sequence generator + */ +class ThreeBackBenchmark { + +} diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index e71780d..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version = 1.2.6 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 0f102c4..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -//addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.3") diff --git a/py/.editorconfig b/py/.editorconfig deleted file mode 100644 index 5345e9e..0000000 --- a/py/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=4 diff --git a/py/.vscode/settings.json b/py/.vscode/settings.json deleted file mode 100644 index f776680..0000000 --- a/py/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": true, - "**/__pycache__": true - }, - "python.pythonPath": "~/.local/share/virtualenvs/py-H3fe-p_n/bin/python" -} \ No newline at end of file diff --git a/py/Pipfile b/py/Pipfile deleted file mode 100644 index 23d2206..0000000 --- a/py/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -name = 'pypi' -url = 'https://pypi.python.org/simple' -verify_ssl = true - -[dev-packages] -pylint = "*" - -[packages] -requests = "*" -python-constraint = "*" -Numberjack = "*" - -[requires] -python_version = "3.7" diff --git a/py/Pipfile.lock b/py/Pipfile.lock deleted file mode 100644 index 25ca7bf..0000000 --- a/py/Pipfile.lock +++ /dev/null @@ -1,149 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8741d50b672e81aecb9c484903354969f4b9e6834a4220355e088e83b03ec6fa" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "numberjack": { - "hashes": [ - "sha256:15fa277a5f4f478d28f7e326ac9273b6cf8d5b854349a7039fee5f18a54779b1" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "python-constraint": { - "hashes": [ - "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", - "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" - ], - "version": "==2.1.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "version": "==4.3.4" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pylint": { - "hashes": [ - "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", - "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" - ], - "index": "pypi", - "version": "==2.2.2" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "wrapt": { - "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" - ], - "version": "==1.11.1" - } - } -} diff --git a/py/README.md b/py/README.md deleted file mode 100644 index 1e7491b..0000000 --- a/py/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# N-Back Sequence Generators - -This project contains Python implementations of various sequence generators for the n-back task. - -## Build and Run - -## Skewed Random Generator - -## Even Random Generator - diff --git a/py/even_random_generator.py b/py/even_random_generator.py deleted file mode 100644 index fda40d8..0000000 --- a/py/even_random_generator.py +++ /dev/null @@ -1,75 +0,0 @@ -from constraint import * - -import itertools as it - -import Numberjack as nj - -class EvenRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials=64, tl=1): - self.tl, self.trials, self.choices = tl, trials, choices - - def generate(self): - seqs = self._generate_initial_sequences() - return self._find_optimal_sequence(seqs, 0.2) - - def _generate_initial_sequences(self): - """ - Generates initial sequence of items based on choices and number of desired trials. - In EvenRandom sequences, all stimuli have same number of appearances. - """ - - pool = it.product(self.choices, repeat=self.trials) - return pool - - def _find_optimal_sequence_numberjack(self,tl_ratio): - """Optimize with Numberjack""" - - cost = nj.Variable(0, 100, 'cost') - seqs = nj.Variable([]) # all sequences - - model = nj.Model( - seqs., - cost == self.calculate_tl_ratio(seq) - tl_ratio, - nj.Minimise(cost) - ) - solver = model.load('Mistral') - if solver.solve(): - solver.printStatistics() - else: - print("No solution with Numberjack") - - - def _find_optimal_sequence(self, sequences, tl_ratio): - """Optimize a sequence to match a desired tl ratio with python-constraints""" - - p = Problem() - - # TODO add all possible values for seq (its domain) - p.addVariable("seq", list(sequences)) - - p.addConstraint(lambda s: self.calculate_tl_ratio(s) - tl_ratio < 0.05) - - return p.getSolution() - - @staticmethod - def calculate_tl_ratio(seq): - """Calculates the T:L ratio of a sequence.""" - targets = 0 - lures = 0 - for index, item in seq: - if item == seq[index-2]: - targets += 1 - elif item == seq[index-1] or item == seq[index-3]: - lures += 1 - # avoid division by zero - if lures == 0: - lures = 1 - return targets/lures - - -if __name__ == '__main__': - generator = EvenRandomGenerator(['a', 'b', 'c'], trials = 4) - generated_seq = generator.generate() - print('Even Random Sequence: %s' % ''.join(generated_seq)) diff --git a/py/ga_optimized_random_generator.py b/py/ga_optimized_random_generator.py deleted file mode 100644 index 4f2ef4c..0000000 --- a/py/ga_optimized_random_generator.py +++ /dev/null @@ -1,134 +0,0 @@ -import itertools as it -import random - - -class GAOptimizedRandomGenerator: - """Generate even random sequences according to a predefined TL ration (Ralph, 2014)""" - - def __init__(self, choices, trials, tl=2.0, pool_size=100, n=3): - """Initialize the genetic algorithm optimizer for n-back sequences. - :param choices: - :param trials: - :param tl: - :param pool_size: - :param n: - """ - self.tl, self.trials, self.choices, self.pool_size, self.n = tl, trials, choices, pool_size, n - self.pool = [] - self.__init_pool(pool_size) - - def generate(self): - """Generate a sequence of trials based on passed parameters. TL ratio and distribution are expected to be - close to the desired ones but not exactly the same. - :return: a sequence of items in "list" format. - """ - generation = 0 - best_parent = self.__find_best_parents(1)[0] - while self.cost(best_parent) > 0.1 and generation < 1000: - generation += 1 - if random.random() > 0.5: - self.pool = list(map(lambda s: self.mutate(s), self.pool)) - self.pool = self.crossover_all() - best_parent = self.__find_best_parents(1)[0] - print(best_parent, 'cost=%f' % self.cost(best_parent)) - return best_parent - - def __init_pool(self, pool_size) -> list: - """ - Initialize solution pool. - :param pool_size: Num of initial random solutions - :return: initial pool of - """ - print("Initializing the pool...") - self.pool.clear() - all_comb = it.combinations_with_replacement(self.choices, self.trials) - sample = random.sample(list(all_comb), pool_size) - self.pool.extend(map(lambda _: ''.join(_), sample)) - return self.pool - - def __find_best_parents(self, count=1): - """ - Find best gene(s) or parent(s) from the current pool. - :param count: Number of desired best parents to be returned. Default is 1. - :return: A list of most fit sequences. - """ - sorted_pool = sorted(self.pool, key=lambda ss: self.cost(ss)) - return sorted_pool[:count] - - def even_dist_cost(self, seq): - """ - Calculate fitness according to the similarity to the desired uniform distribution. - :param seq: a string - :return: - """ - costs = {c: 0.0 for c in self.choices} - for c in list(seq): - costs[c] += (1.0 if costs.__contains__(c) else 0.0) - costs = {k: abs(1.0 - v*len(self.choices)/self.trials) for k,v in costs.items()} - return max(list(costs.values())) - - def cost(self, seq): - """ - Calculate overall fitness of a sequence (block of trials). - Right now it's a cost function, so we try to minimize this cost. - :param seq: - :return: - """ - # add fitness for uniform distribution of all stimuli - return abs(self.calculate_tl_ratio(seq, self.n) - self.tl) + self.even_dist_cost(seq) - - def crossover_all(self): - """ - Perform random crossover for all pairs. - :return: new pool - """ - new_pool = [] - for i in range(int(self.pool_size/2)): - seq1 = self.pool[i*2] # change to weighted random - seq2 = self.pool[i*2 + 1] # change to weighted random - new_pool.extend(self.crossover(seq1, seq2)) - - return new_pool - - def crossover(self, seq1, seq2): - """ - Crossover two sequences. - :param seq1: - :param seq2: - :return: - """ - pos = random.randint(0, self.trials) - return [seq1[:pos] + seq2[pos:], seq2[:pos] + seq1[pos:]] - - def mutate(self, seq): - if random.random() > 0.5: - pos = random.randint(0, len(seq)-1) - seq_list = list(seq) - seq_list[pos] = random.choice(self.choices) - return ''.join(seq_list) - return seq - - @staticmethod - def calculate_tl_ratio(seq, n: int): - """Calculates the T/L ratio in a block of trials.""" - targets = 0.0 - lures = 0.0 - for index in range(n, len(seq)): - item = seq[index] - if item == seq[index - n]: - targets += 1.0 - elif item == seq[index - (n-1)] or item == seq[index - (n+1)]: - lures += 1.0 - if lures - 0.0 < 0.001: # avoid division by zero - lures = 0.001 - return targets/lures - - -if __name__ == '__main__': - - generator = GAOptimizedRandomGenerator(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], trials=16, n=2) - sq = generator.generate() - tl_ratio = generator.calculate_tl_ratio(sq, n=2) - even_dist = generator.even_dist_cost(sq) - - print('GA-Optimized Sequence: %s' % sq, 'with tl_ratio=%f' % tl_ratio, 'and even_dist_cost=%f' % even_dist) diff --git a/py/main.py b/py/main.py deleted file mode 100644 index 84f16d3..0000000 --- a/py/main.py +++ /dev/null @@ -1,5 +0,0 @@ -import requests - -response = requests.get('https://httpbin.org/ip') - -print('Testing connection... Your IP is %s.' % response.json()['origin']) \ No newline at end of file diff --git a/py/skewed_random_generator.py b/py/skewed_random_generator.py deleted file mode 100644 index 5cae6e8..0000000 --- a/py/skewed_random_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -import random - -class SkewedRandomGenerator: - """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" - - seq = [] - - def __init__(self, - N = 2, - trials = 100, # Number of total trials - alphabet=['A','B','C','D','E','F'], - T = 20, # Number of targets - L1 = 10, # Number of lures (foil) similar to the (N+1)-back - L2 = 10 # Number of lures (foil) similar to the (N-1)-back - ): - self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 - self.D = trials - T - L1 - L2 - - def generate(self) -> list: - trial = 1 - self.seq = [] - while trial <= self.trials: - self.seq += self.random_stimulus(trial) - trial += 1 - return self.seq - - def random_stimulus(self, trial): - rnd = random.randint(1, self.trials - trial + 1) - T, L1, L2 = self.T, self.L1, self.L2 - if rnd <= T and len(self.seq) >= self.N: - self.T -= 1 - return self.seq[-self.N] - elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: - self.L1 -= 1 - return self.seq[-(self.N+1)] - elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: - self.L2 -= 1 - return self.seq[-(self.N-1)] - - # distract - self.D -= 1 - alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] - return random.choice(alphabet) - - -if __name__ == '__main__': - generator = SkewedRandomGenerator() - seq = generator.generate() - print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/skewed_random_generator.py b/skewed_random_generator.py new file mode 100644 index 0000000..5cae6e8 --- /dev/null +++ b/skewed_random_generator.py @@ -0,0 +1,50 @@ +import logging +import random + +class SkewedRandomGenerator: + """Generates random sequence of stimuli for the n-back task. Implementation is based on Ralph (2014).""" + + seq = [] + + def __init__(self, + N = 2, + trials = 100, # Number of total trials + alphabet=['A','B','C','D','E','F'], + T = 20, # Number of targets + L1 = 10, # Number of lures (foil) similar to the (N+1)-back + L2 = 10 # Number of lures (foil) similar to the (N-1)-back + ): + self.N, self.alphabet, self.trials, self.T, self.L1, self.L2 = N, alphabet, trials, T, L1, L2 + self.D = trials - T - L1 - L2 + + def generate(self) -> list: + trial = 1 + self.seq = [] + while trial <= self.trials: + self.seq += self.random_stimulus(trial) + trial += 1 + return self.seq + + def random_stimulus(self, trial): + rnd = random.randint(1, self.trials - trial + 1) + T, L1, L2 = self.T, self.L1, self.L2 + if rnd <= T and len(self.seq) >= self.N: + self.T -= 1 + return self.seq[-self.N] + elif T < rnd <= T + L1 and len(self.seq) >= self.N+1: + self.L1 -= 1 + return self.seq[-(self.N+1)] + elif T + L1 < rnd <= T + L1 + L2 and len(self.seq) >= self.N-1: + self.L2 -= 1 + return self.seq[-(self.N-1)] + + # distract + self.D -= 1 + alphabet = [item for item in self.alphabet if item not in self.seq[-self.N-1:-self.N+1]] + return random.choice(alphabet) + + +if __name__ == '__main__': + generator = SkewedRandomGenerator() + seq = generator.generate() + print('Skewed Random Sequence: %s' % ''.join(seq)) diff --git a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala deleted file mode 100644 index 021ea42..0000000 --- a/src/main/scala/org/xcit/nback/generators/EvenSequenceGenerator.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable -import scala.util.Random - -/** - * Generate uniformly random alphanumeric string of size N, with target item evenly distributed. - * Reference: Ralph2014 - * - * @param alphabet All allowed alphabets (default is "BCDEFGJKLMNQRSQVWXYZ" excluding target and similar - * characters). Default alphabet contains 20 characters. - * @param target target item (default is 'A'). - */ -class EvenSequenceGenerator(alphabet: String = "BCDEFGJKLMNQRSQVWXYZ", target: Char = 'A') extends SequenceGenerator { - /** - * Generate the actual sequence as a string, takes only the length of desired sequence (num of trials) - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 64): String = { - var results = mutable.ListBuffer[String]() - val items = List.range(0, trials) - - // decide if target or non-target with 50/50 chance. - //TODO change probability according to a new class param - if (Random.nextBoolean) - results += target.toString - else - items.foreach( - results += alphabet.charAt(Random.nextInt(alphabet.length)).toString - ) - results.mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala deleted file mode 100644 index d10e633..0000000 --- a/src/main/scala/org/xcit/nback/generators/MarkovChainSequenceGenerator.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.xcit.nback.generators - -//TODO -class MarkovChainSequenceGenerator { - -} diff --git a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala deleted file mode 100644 index 8488984..0000000 --- a/src/main/scala/org/xcit/nback/generators/RandomSequenceGenerator.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.generators - -import scala.util.Random - -class RandomSequenceGenerator(N: Int) extends SequenceGenerator { - /** - * Generate simple alphanumeric string of size N - * - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(trials: Int = 0): String = { - Random.alphanumeric.take(trials).mkString("") - } -} diff --git a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala deleted file mode 100644 index 9cb0604..0000000 --- a/src/main/scala/org/xcit/nback/generators/SequenceGenerator.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.xcit.nback.generators - -/** - * Companion object and buiilders interface for Strategy pattern of the sequence generators. - * Params: - * "n": The "n" property in n-back (N items back must match the current stimuli). - * "offline": if it should generate the whole sequence at once of use MDP to consider rewards with reinforcement - * learning methods. - * - */ -object SequenceGenerator { - def random(N: Int = 3, trials: Int = 0): String = new RandomSequenceGenerator(N).generate(trials) - def even(N: Int = 3, trials: Int = 0, alphabet: String = "bcdg", target: Char = 'a'): String = new EvenSequenceGenerator().generate(trials) - def skewed() = new SkewedSequenceGenerator(T = 24, D = 32, L1 = 4, L2 = 4).generate(64) -} - -/** - * Generic n-back class to generate a sequence of n-back items with HMM. - */ -trait SequenceGenerator { - - /** - * Generate simple alphanumeric string of size N - * @param trials number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - def generate(trials: Int = 0): String -} diff --git a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala b/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala deleted file mode 100644 index 3549506..0000000 --- a/src/main/scala/org/xcit/nback/generators/SkewedSequenceGenerator.scala +++ /dev/null @@ -1,89 +0,0 @@ -package org.xcit.nback.generators - -import scala.collection.mutable.ListBuffer -import scala.util.Random - -/** - * Reference: Ralph2014 - Appendix A - * - * @param alphabet Alphabet used to generate the sequence, excluding the target item - * @param target Target item. It must not be included in [[alphabet]]. - * @param N The set size of stimuli to remember (n in n-back). Default is 3. - * @param T Number of target trials - * @param D Number of non-matching distractor trials. - * @param L1 Number of pre-lure trials; new trial matches with n-1 trial (pre-trial match). - * @param L2 Number of post-lure trials; trial matches with n+1 trial (post-trial match). - */ -class SkewedSequenceGenerator( - alphabet: String = "ABCDEFGH", // it's also called pool in ralph2014 - target: Char = 'A', - N: Int = 3, - T: Int = 24, - D: Int = 32, - L1: Int = 4, - L2: Int = 4) - extends SequenceGenerator { - - - val trials = T + D + L1 + L2 - - /** - * Types of each trial (L1, L2, T, or D). - */ - private object TrialType extends Enumeration { - val LURE_BEFORE_TARGET, LURE_AFTER_TARGET, TARGET, DISTRACTOR = Value - } - - /** - * Ignores trials. Trials' value is defined by N+T+D+L1+L2 - * @param t number of items in n-back sequence. - * @return string of size "trials" with random alphanumeric characters. - */ - override def generate(t: Int): String = { - // 1. Init buffer with empty list of items :-). Why am I explaining this? - var buffer = ListBuffer[String]() - - var trial = 0 - - while (trial < trials) { - trial += 1 - - // 2. For each trial, generate a random type (L1, L2, T, or D) - // 3. Based on the generated type, generate the item and append it to the buffer - buffer += {nextTrialType(trials-trial) match { - case TrialType.LURE_BEFORE_TARGET if buffer.length > N + 1 => - buffer(buffer.length - (N + 2)) - case TrialType.TARGET if buffer.length > N => - buffer(buffer.length - (N - 1)) - case TrialType.LURE_BEFORE_TARGET if buffer.length > N => - buffer(buffer.length - N) - case _ => - alphabet.charAt(Random.nextInt(alphabet.length)).toString - }} - - } - // 4. Convert buffer to string and return it as result - buffer.mkString("") - } - - /** - * Defines next trial types randomly. It returns L1, L2, T, or D (Ralph2014). - * Random range: [1, remaining_trials] (both are inclusive) - * @return a trial type to be used to generate an item for current trial - */ - private def nextTrialType(remainingTrials: Int): TrialType.Value = { Random.nextInt(remainingTrials + 1) + 1 } match { - case rnd if rnd <= L1 => - L1 -= 1 - TrialType.LURE_BEFORE_TARGET - case rnd if rnd > L1 && rnd <= L1 + L2 => - L2 -= 1 - TrialType.LURE_AFTER_TARGET - case rnd if rnd > L1 + L2 && rnd <= L1 + L2 + T => - T -= 1 - TrialType.TARGET - case _ => - D -= 1 - TrialType.DISTRACTOR - - } -} diff --git a/src/main/scala/org/xcit/nback/markov/Main.scala b/src/main/scala/org/xcit/nback/markov/Main.scala deleted file mode 100644 index fdcb58b..0000000 --- a/src/main/scala/org/xcit/nback/markov/Main.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.xcit.nback.markov - -/** - * Default application runner to run some benchmarks and demos. Use `sbt test` for the actual bechmarks. - */ -class Main extends App { - //TODO create a MarkovChain object or use the singleton - //TODO train to set transitions - //TODO benchmark -} diff --git a/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala b/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala deleted file mode 100644 index 5694371..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/MarkovChain.scala +++ /dev/null @@ -1,39 +0,0 @@ -package org.xcit.nback.markov.models - -import scala.collection.mutable - -/** - * Represents a simple markov chain - * TODO: must be extended to HMM (add emission probability) - */ -class MarkovChain(startState: State) { - val states: mutable.MutableList[State] = mutable.MutableList[State]() - - var currentState: State = startState - - //TODO - def next(): State = ??? - - //def currentProbability(): Double = currentState.transitions.size.toDouble / totalTransitions().toDouble - - def totalTransitions(): Int = { - var total = 0 - states.foreach(total += _.transitions.size) - total - } - - /** - * Add a transition from "from" state to "to" state with a defined probability. - * @param from origin node - * @param to destination node - * @param probability transition probability - */ - def addTransition(from: State, to: State, probability: Double): Unit = - states - .find(_.label == from.label) - .map(s => - s.transitions += Transition(to, probability) - ) - - -} diff --git a/src/main/scala/org/xcit/nback/markov/models/State.scala b/src/main/scala/org/xcit/nback/markov/models/State.scala deleted file mode 100644 index fbbd844..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/State.scala +++ /dev/null @@ -1,15 +0,0 @@ -package org.xcit.nback.markov.models - -import scala.collection.mutable - -/** - * A Single state with a label in Markov chain - * @param label A simple string label, representing the state and node - */ -class State(val label: String) { - - /** - * Keep track of transitions from this state to other states. - */ - val transitions: mutable.MutableList[Transition] = mutable.MutableList[Transition]() -} diff --git a/src/main/scala/org/xcit/nback/markov/models/Transition.scala b/src/main/scala/org/xcit/nback/markov/models/Transition.scala deleted file mode 100644 index bc20a9e..0000000 --- a/src/main/scala/org/xcit/nback/markov/models/Transition.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.xcit.nback.markov.models - -/** - * A Single transition in Markov chain graph. It is stored in "from" node. - * @param to the ending node of the edge - * @param probability the probability of this transition from starting node to the ending node (0.0 <= p <= 1.0). - */ -case class Transition(to: State, probability: Double = 0.0) diff --git a/src/test/scala/ThreeBackBenchmark.scala b/src/test/scala/ThreeBackBenchmark.scala deleted file mode 100644 index 0c7dad9..0000000 --- a/src/test/scala/ThreeBackBenchmark.scala +++ /dev/null @@ -1,7 +0,0 @@ - -/** - * TODO Benchmark 3-back simple sequence generator - */ -class ThreeBackBenchmark { - -}