├── .github └── workflows │ ├── deploy.yaml │ └── test.yaml ├── .gitignore ├── .gitmodules ├── LICENSE ├── MANIFEST.in ├── README.md ├── arcsolver ├── __init__.py ├── _modidx.py ├── describe.py ├── examples.py ├── ocm.py ├── ocm_cleaned.py ├── score.py ├── solve.py ├── task.py └── utils.py ├── index_files ├── figure-commonmark │ ├── cell-2-output-1.png │ └── cell-4-output-1.png └── figure-html │ ├── cell-2-output-1.png │ └── cell-4-output-1.png ├── nbs ├── 00_task.ipynb ├── 01_ocm.ipynb ├── 02_describe.ipynb ├── 03_solve.ipynb ├── 04_utils.ipynb ├── 05_examples.ipynb ├── 06_score.ipynb ├── _quarto.yml ├── index.ipynb ├── nbdev.yml ├── sidebar.yml └── styles.css ├── pyproject.toml ├── settings.ini └── setup.py /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | permissions: 4 | contents: write 5 | pages: write 6 | 7 | on: 8 | push: 9 | branches: [ "main", "master" ] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | - uses: fastai/workflows/quarto-ghp@master 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [workflow_dispatch, pull_request, push] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | with: 10 | submodules: recursive 11 | - uses: fastai/workflows/nbdev-ci@master 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _docs/ 2 | _proc/ 3 | 4 | *.bak 5 | .gitattributes 6 | .last_checked 7 | .gitconfig 8 | *.bak 9 | *.log 10 | *~ 11 | ~* 12 | _tmp* 13 | tmp* 14 | tags 15 | *.pkg 16 | 17 | # Byte-compiled / optimized / DLL files 18 | __pycache__/ 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | env/ 28 | build/ 29 | conda/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *.cover 64 | .hypothesis/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # dotenv 100 | .env 101 | 102 | # virtualenv 103 | .venv 104 | venv/ 105 | ENV/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | 120 | .vscode 121 | *.swp 122 | 123 | # osx generated files 124 | .DS_Store 125 | .DS_Store? 126 | .Trashes 127 | ehthumbs.db 128 | Thumbs.db 129 | .idea 130 | 131 | # pytest 132 | .pytest_cache 133 | 134 | # tools/trust-doc-nbs 135 | docs_src/.last_checked 136 | 137 | # symlinks to fastai 138 | docs_src/fastai 139 | tools/fastai 140 | 141 | # link checker 142 | checklink/cookies.txt 143 | 144 | # .gitconfig is now autogenerated 145 | .gitconfig 146 | 147 | # Quarto installer 148 | .deb 149 | .pkg 150 | 151 | # Quarto 152 | .quarto 153 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "arc_data"] 2 | path = arcsolver/arc_data 3 | url = https://github.com/fchollet/ARC-AGI.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022, fastai 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include settings.ini 2 | include LICENSE 3 | include CONTRIBUTING.md 4 | include README.md 5 | recursive-exclude * __pycache__ 6 | recursive-include arcsolver/arc_data/data *.json 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arcsolver 2 | 3 | 4 | 5 | 6 | This library contains tools for visualizing, analyzing and solving tasks 7 | from the Abstraction and Reasoning Corpus (ARC) challenge dataset. 8 | 9 | As this library was built using 10 | [`nbdev`](https://github.com/AnswerDotAI/claudette.git), the source code 11 | can be found in the jupyter notebooks directory 12 | ([nbs](https://github.com/agemoai/arcsolver/tree/main/nbs)). 13 | 14 | Full documentation available at https://agemoai.github.io/arcsolver. 15 | 16 | ## Installation 17 | 18 | 1. Install `claudette` from its GitHub 19 | [repository](https://github.com/AnswerDotAI/claudette) (PyPi version 20 | is a bit behind): 21 | 22 | ``` sh 23 | $ pip install git+https://github.com/AnswerDotAI/claudette.git@5ea3a59 24 | ``` 25 | 26 | 2. Install `arcsolver`: 27 | 28 | ``` sh 29 | $ pip install arcsolver 30 | ``` 31 | 32 | > [!NOTE] 33 | > 34 | > To use the automated description or solution generation features of 35 | > this library, access to Anthropic’s Claude Sonnet 3.5 model is 36 | > required. Set the `ANTHROPIC_API_KEY` environment variable or 37 | > configure appropriate credentials for AWS bedrock or Google Vertex. 38 | 39 | ## Key Features 40 | 41 | - **Task Management:** Load and visualize ARC tasks with the 42 | [`ArcTask`](https://agemoai.github.io/arcsolver/task.html#arctask) 43 | class 44 | - **Object-Centric Modelling:** A set of primitive classes for 45 | representing grid objects and transformations 46 | - **LLM Integration:** Designed to use Claude Sonnet 3.5 for automated 47 | task analysis and solution generation 48 | - **Extensible Architecture:** Easy to add new primitives and helper 49 | functions to enhance solving capabilities 50 | 51 | ## Quick Start 52 | 53 | ### Task Representation 54 | 55 | The `task` module provides classes for working with ARC tasks 56 | 57 | ``` python 58 | from arcsolver.task import ArcGrid, ArcPair, ArcTask 59 | 60 | task = ArcTask('1e0a9b12'); task.plot() 61 | ``` 62 | 63 | ![](index_files/figure-commonmark/cell-2-output-1.png) 64 | 65 | An [`ArcTask`](https://agemoai.github.io/arcsolver/task.html#arctask) 66 | comprises a list of input-output example 67 | [`ArcPair`](https://agemoai.github.io/arcsolver/task.html#arcpair)s, 68 | each of which holds two 69 | [`ArcGrid`](https://agemoai.github.io/arcsolver/task.html#arcgrid)s. 70 | Each class has convenient `plot` methods for visualization or directly 71 | outputting to binary strings that can be passed to Claude. 72 | 73 | ``` python 74 | print(f"Input grid 1 plot: {task.train[0].input.plot(to_base64=True)[:20]}...") 75 | ``` 76 | 77 | Input grid 1 plot: b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01H'... 78 | 79 | ### Object-centric Models 80 | 81 | The `ocm` module provides a set of primitive classes for constructing 82 | object-centric models of ARC grids. For example: 83 | 84 | ``` python 85 | from arcsolver.ocm import Vector, Rectangle, Line, Grid, Color, Direction 86 | 87 | grid = Grid( 88 | size=Vector(8,8), 89 | background_color=Color('grey'), 90 | objects=[ 91 | Rectangle(position=Vector(1,1), size=Vector(2,2), color=Color('red')), 92 | Line(position=Vector(6,1), direction=Direction.NE, length=6, color=Color('pink')) 93 | ] 94 | ) 95 | ArcGrid(grid.to_array()).plot() 96 | ``` 97 | 98 | ![](index_files/figure-commonmark/cell-4-output-1.png) 99 | 100 | ### Task Descriptions 101 | 102 | Use Claude to analyze and describe ARC tasks 103 | 104 | ``` python 105 | from arcsolver.describe import DescriptionGenerator 106 | 107 | describer = DescriptionGenerator() 108 | d = await describer.describe_task(task, 1); print(d[0].d) 109 | ``` 110 | 111 | > The input grids contain various colored squares arranged on a black 112 | > background in different positions. In the transformation, all colored 113 | > squares “fall” vertically to the bottom row while maintaining their 114 | > relative horizontal positions and original colors. The rest of the 115 | > grid becomes filled with black squares, resulting in an output where 116 | > all non-black squares are aligned in the bottom row, preserving their 117 | > left-to-right ordering from the input grid. 118 | 119 | > [!WARNING] 120 | > 121 | > Depending on the task and the description strategy used (see 122 | > [docs](https://agemoai.github.io/arcsolver/describe.html)), 123 | > [`DescriptionGenerator`](https://agemoai.github.io/arcsolver/describe.html#descriptiongenerator) 124 | > may decompose the task into multiple images, resulting in a 125 | > token-intensive prompt (~\$0.10 using Sonnet 3.5). 126 | 127 | ### Solution Generation 128 | 129 | Use Claude to construct a solution to an ARC task, automatically 130 | refining its attempts based on execution and prediction error feedback. 131 | 132 | ``` python 133 | from arcsolver.solve import ArcSolver 134 | 135 | solver = ArcSolver() 136 | solutions = await solver.solve(task) 137 | ``` 138 | 139 | 140 | Solving task: 1e0a9b12 141 | Generating descriptions... | Attempts: 0/30 | Best Score: 0.000 | Cost: $0.000 142 | Starting solution attempts... | Attempts: 0/30 | Best Score: 0.000 | Cost: $0.142 143 | Generating initial solutions... | Attempts: 0/30 | Best Score: 0.000 | Cost: $0.142 144 | Testing solutions... | Attempts: 0/30 | Best Score: 0.000 | Cost: $0.231 145 | Continuing refinement... | Attempts: 2/30 | Best Score: 0.866 | Cost: $0.231 146 | Refining previous solutions... | Attempts: 2/30 | Best Score: 0.866 | Cost: $0.231 147 | Testing solutions... | Attempts: 2/30 | Best Score: 0.866 | Cost: $0.332 148 | Continuing refinement... | Attempts: 4/30 | Best Score: 0.904 | Cost: $0.332 149 | Refining previous solutions... | Attempts: 4/30 | Best Score: 0.904 | Cost: $0.332 150 | Testing solutions... | Attempts: 4/30 | Best Score: 0.904 | Cost: $0.424 151 | Continuing refinement... | Attempts: 6/30 | Best Score: 0.951 | Cost: $0.424 152 | Refining previous solutions... | Attempts: 6/30 | Best Score: 0.951 | Cost: $0.424 153 | Testing solutions... | Attempts: 6/30 | Best Score: 0.951 | Cost: $0.528 154 | Continuing refinement... | Attempts: 8/30 | Best Score: 0.951 | Cost: $0.528 155 | Refining previous solutions... | Attempts: 8/30 | Best Score: 0.951 | Cost: $0.528 156 | Testing solutions... | Attempts: 8/30 | Best Score: 0.951 | Cost: $0.633 157 | Continuing refinement... | Attempts: 10/30 | Best Score: 0.958 | Cost: $0.633 158 | Refining previous solutions... | Attempts: 10/30 | Best Score: 0.958 | Cost: $0.633 159 | Testing solutions... | Attempts: 10/30 | Best Score: 0.958 | Cost: $0.732 160 | Continuing refinement... | Attempts: 12/30 | Best Score: 0.965 | Cost: $0.732 161 | Refining previous solutions... | Attempts: 12/30 | Best Score: 0.965 | Cost: $0.732 162 | Testing solutions... | Attempts: 12/30 | Best Score: 0.965 | Cost: $0.835 163 | Found potential solution, validating... | Attempts: 12/30 | Best Score: 1.000 | Cost: $0.835 164 | Solution found! | Attempts: 14/30 | Best Score: 1.000 | Cost: $0.835 165 | Solution found! 🎉 | Attempts: 14/30 | Best Score: 1.000 | Cost: $0.835 166 | 167 | ## Contributing 168 | 169 | Contributions are welcome! Refined prompts, new OCM primitives, expanded 170 | tool-use, alternative retry strategy… 171 | 172 | Feel free to raise an issue or submit a PR. 173 | 174 | ### Learn More 175 | 176 | To read about the motivation for building this tool, check out our 177 | [blog](https://agemo.ai/resources/summer-of-arc-agi) and watch out for 178 | future posts 179 | -------------------------------------------------------------------------------- /arcsolver/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.11" 2 | -------------------------------------------------------------------------------- /arcsolver/_modidx.py: -------------------------------------------------------------------------------- 1 | # Autogenerated by nbdev 2 | 3 | d = { 'settings': { 'branch': 'main', 4 | 'doc_baseurl': '/arcsolver', 5 | 'doc_host': 'https://agemoai.github.io', 6 | 'git_url': 'https://github.com/agemoai/arcsolver', 7 | 'lib_path': 'arcsolver'}, 8 | 'syms': { 'arcsolver.describe': { 'arcsolver.describe.AsyncChat.__call__': ('describe.html#asyncchat.__call__', 'arcsolver/describe.py'), 9 | 'arcsolver.describe.AsyncChat.toolloop': ('describe.html#asyncchat.toolloop', 'arcsolver/describe.py'), 10 | 'arcsolver.describe.Description': ('describe.html#description', 'arcsolver/describe.py'), 11 | 'arcsolver.describe.Description.cost': ('describe.html#description.cost', 'arcsolver/describe.py'), 12 | 'arcsolver.describe.Description.d': ('describe.html#description.d', 'arcsolver/describe.py'), 13 | 'arcsolver.describe.Description.usage': ('describe.html#description.usage', 'arcsolver/describe.py'), 14 | 'arcsolver.describe.DescriptionGenerator': ( 'describe.html#descriptiongenerator', 15 | 'arcsolver/describe.py'), 16 | 'arcsolver.describe.DescriptionGenerator.__init__': ( 'describe.html#descriptiongenerator.__init__', 17 | 'arcsolver/describe.py'), 18 | 'arcsolver.describe.DescriptionGenerator._create_chat': ( 'describe.html#descriptiongenerator._create_chat', 19 | 'arcsolver/describe.py'), 20 | 'arcsolver.describe.DescriptionGenerator._create_client': ( 'describe.html#descriptiongenerator._create_client', 21 | 'arcsolver/describe.py'), 22 | 'arcsolver.describe.DescriptionGenerator.describe_direct': ( 'describe.html#descriptiongenerator.describe_direct', 23 | 'arcsolver/describe.py'), 24 | 'arcsolver.describe.DescriptionGenerator.describe_indirect': ( 'describe.html#descriptiongenerator.describe_indirect', 25 | 'arcsolver/describe.py'), 26 | 'arcsolver.describe.DescriptionGenerator.describe_task': ( 'describe.html#descriptiongenerator.describe_task', 27 | 'arcsolver/describe.py'), 28 | 'arcsolver.describe.ShapeExtractor': ('describe.html#shapeextractor', 'arcsolver/describe.py'), 29 | 'arcsolver.describe.ShapeExtractor.__init__': ( 'describe.html#shapeextractor.__init__', 30 | 'arcsolver/describe.py'), 31 | 'arcsolver.describe.ShapeExtractor.extract_shapes': ( 'describe.html#shapeextractor.extract_shapes', 32 | 'arcsolver/describe.py'), 33 | 'arcsolver.describe._create_chat': ('describe.html#_create_chat', 'arcsolver/describe.py'), 34 | 'arcsolver.describe._create_client': ('describe.html#_create_client', 'arcsolver/describe.py'), 35 | 'arcsolver.describe._describe_direct': ('describe.html#_describe_direct', 'arcsolver/describe.py'), 36 | 'arcsolver.describe._describe_indirect': ('describe.html#_describe_indirect', 'arcsolver/describe.py'), 37 | 'arcsolver.describe._pair_prompt': ('describe.html#_pair_prompt', 'arcsolver/describe.py'), 38 | 'arcsolver.describe._shape_table': ('describe.html#_shape_table', 'arcsolver/describe.py')}, 39 | 'arcsolver.examples': {'arcsolver.examples.Example': ('examples.html#example', 'arcsolver/examples.py')}, 40 | 'arcsolver.ocm': { 'arcsolver.ocm.Bitmap': ('ocm.html#bitmap', 'arcsolver/ocm.py'), 41 | 'arcsolver.ocm.Bitmap._get_shape_array': ('ocm.html#bitmap._get_shape_array', 'arcsolver/ocm.py'), 42 | 'arcsolver.ocm.Bitmap.flip': ('ocm.html#bitmap.flip', 'arcsolver/ocm.py'), 43 | 'arcsolver.ocm.Bitmap.rotate': ('ocm.html#bitmap.rotate', 'arcsolver/ocm.py'), 44 | 'arcsolver.ocm.Bitmap.size': ('ocm.html#bitmap.size', 'arcsolver/ocm.py'), 45 | 'arcsolver.ocm.Bitmap.validate_data': ('ocm.html#bitmap.validate_data', 'arcsolver/ocm.py'), 46 | 'arcsolver.ocm.Color': ('ocm.html#color', 'arcsolver/ocm.py'), 47 | 'arcsolver.ocm.Color.__init__': ('ocm.html#color.__init__', 'arcsolver/ocm.py'), 48 | 'arcsolver.ocm.Color.__str__': ('ocm.html#color.__str__', 'arcsolver/ocm.py'), 49 | 'arcsolver.ocm.Color.to_array': ('ocm.html#color.to_array', 'arcsolver/ocm.py'), 50 | 'arcsolver.ocm.CyclicPattern': ('ocm.html#cyclicpattern', 'arcsolver/ocm.py'), 51 | 'arcsolver.ocm.CyclicPattern.__len__': ('ocm.html#cyclicpattern.__len__', 'arcsolver/ocm.py'), 52 | 'arcsolver.ocm.CyclicPattern.extend': ('ocm.html#cyclicpattern.extend', 'arcsolver/ocm.py'), 53 | 'arcsolver.ocm.CyclicPattern.find_period': ('ocm.html#cyclicpattern.find_period', 'arcsolver/ocm.py'), 54 | 'arcsolver.ocm.CyclicPattern.from_array': ('ocm.html#cyclicpattern.from_array', 'arcsolver/ocm.py'), 55 | 'arcsolver.ocm.CyclicPattern.to_array': ('ocm.html#cyclicpattern.to_array', 'arcsolver/ocm.py'), 56 | 'arcsolver.ocm.Direction': ('ocm.html#direction', 'arcsolver/ocm.py'), 57 | 'arcsolver.ocm.Direction.from_array': ('ocm.html#direction.from_array', 'arcsolver/ocm.py'), 58 | 'arcsolver.ocm.Direction.to_array': ('ocm.html#direction.to_array', 'arcsolver/ocm.py'), 59 | 'arcsolver.ocm.EnclosureFiller': ('ocm.html#enclosurefiller', 'arcsolver/ocm.py'), 60 | 'arcsolver.ocm.EnclosureFiller.fill_enclosures': ( 'ocm.html#enclosurefiller.fill_enclosures', 61 | 'arcsolver/ocm.py'), 62 | 'arcsolver.ocm.Grid': ('ocm.html#grid', 'arcsolver/ocm.py'), 63 | 'arcsolver.ocm.Grid._create_grid': ('ocm.html#grid._create_grid', 'arcsolver/ocm.py'), 64 | 'arcsolver.ocm.Grid.to_array': ('ocm.html#grid.to_array', 'arcsolver/ocm.py'), 65 | 'arcsolver.ocm.Grid.validate_grid_coverage': ('ocm.html#grid.validate_grid_coverage', 'arcsolver/ocm.py'), 66 | 'arcsolver.ocm.Line': ('ocm.html#line', 'arcsolver/ocm.py'), 67 | 'arcsolver.ocm.Line._get_shape_array': ('ocm.html#line._get_shape_array', 'arcsolver/ocm.py'), 68 | 'arcsolver.ocm.Line.offset': ('ocm.html#line.offset', 'arcsolver/ocm.py'), 69 | 'arcsolver.ocm.Object': ('ocm.html#object', 'arcsolver/ocm.py'), 70 | 'arcsolver.ocm.Object._get_shape_array': ('ocm.html#object._get_shape_array', 'arcsolver/ocm.py'), 71 | 'arcsolver.ocm.Object.to_array': ('ocm.html#object.to_array', 'arcsolver/ocm.py'), 72 | 'arcsolver.ocm.PatternMatcher': ('ocm.html#patternmatcher', 'arcsolver/ocm.py'), 73 | 'arcsolver.ocm.PatternMatcher.extract_matching_region': ( 'ocm.html#patternmatcher.extract_matching_region', 74 | 'arcsolver/ocm.py'), 75 | 'arcsolver.ocm.PatternMatcher.find_best_match': ( 'ocm.html#patternmatcher.find_best_match', 76 | 'arcsolver/ocm.py'), 77 | 'arcsolver.ocm.PatternMatcher.find_matches': ('ocm.html#patternmatcher.find_matches', 'arcsolver/ocm.py'), 78 | 'arcsolver.ocm.Rectangle': ('ocm.html#rectangle', 'arcsolver/ocm.py'), 79 | 'arcsolver.ocm.Rectangle._get_shape_array': ('ocm.html#rectangle._get_shape_array', 'arcsolver/ocm.py'), 80 | 'arcsolver.ocm.ShapeExtractor': ('ocm.html#shapeextractor', 'arcsolver/ocm.py'), 81 | 'arcsolver.ocm.ShapeExtractor.extract_all_shapes': ( 'ocm.html#shapeextractor.extract_all_shapes', 82 | 'arcsolver/ocm.py'), 83 | 'arcsolver.ocm.ShapeExtractor.extract_contiguous_regions': ( 'ocm.html#shapeextractor.extract_contiguous_regions', 84 | 'arcsolver/ocm.py'), 85 | 'arcsolver.ocm.ShapeExtractor.extract_largest_shape': ( 'ocm.html#shapeextractor.extract_largest_shape', 86 | 'arcsolver/ocm.py'), 87 | 'arcsolver.ocm.Vector': ('ocm.html#vector', 'arcsolver/ocm.py'), 88 | 'arcsolver.ocm.Vector.__add__': ('ocm.html#vector.__add__', 'arcsolver/ocm.py'), 89 | 'arcsolver.ocm.Vector.__init__': ('ocm.html#vector.__init__', 'arcsolver/ocm.py'), 90 | 'arcsolver.ocm.Vector.__mul__': ('ocm.html#vector.__mul__', 'arcsolver/ocm.py'), 91 | 'arcsolver.ocm.Vector.__rmul__': ('ocm.html#vector.__rmul__', 'arcsolver/ocm.py'), 92 | 'arcsolver.ocm.Vector.__sub__': ('ocm.html#vector.__sub__', 'arcsolver/ocm.py'), 93 | 'arcsolver.ocm.Vector.from_array': ('ocm.html#vector.from_array', 'arcsolver/ocm.py'), 94 | 'arcsolver.ocm.Vector.to_array': ('ocm.html#vector.to_array', 'arcsolver/ocm.py')}, 95 | 'arcsolver.ocm_cleaned': {}, 96 | 'arcsolver.score': {'arcsolver.score.score': ('score.html#score', 'arcsolver/score.py')}, 97 | 'arcsolver.solve': { 'arcsolver.solve.ArcSolver': ('solve.html#arcsolver', 'arcsolver/solve.py'), 98 | 'arcsolver.solve.ArcSolver.__init__': ('solve.html#arcsolver.__init__', 'arcsolver/solve.py'), 99 | 'arcsolver.solve.ArcSolver._collect_attempts': ( 'solve.html#arcsolver._collect_attempts', 100 | 'arcsolver/solve.py'), 101 | 'arcsolver.solve.ArcSolver._count_attempts': ( 'solve.html#arcsolver._count_attempts', 102 | 'arcsolver/solve.py'), 103 | 'arcsolver.solve.ArcSolver._get_best_n': ('solve.html#arcsolver._get_best_n', 'arcsolver/solve.py'), 104 | 'arcsolver.solve.ArcSolver._log_progress': ('solve.html#arcsolver._log_progress', 'arcsolver/solve.py'), 105 | 'arcsolver.solve.ArcSolver.solve': ('solve.html#arcsolver.solve', 'arcsolver/solve.py'), 106 | 'arcsolver.solve.AsyncChat.codeloop': ('solve.html#asyncchat.codeloop', 'arcsolver/solve.py'), 107 | 'arcsolver.solve.Attempt': ('solve.html#attempt', 'arcsolver/solve.py'), 108 | 'arcsolver.solve.Attempt.score': ('solve.html#attempt.score', 'arcsolver/solve.py'), 109 | 'arcsolver.solve.CodeValidator': ('solve.html#codevalidator', 'arcsolver/solve.py'), 110 | 'arcsolver.solve.CodeValidator.format_syntax_error': ( 'solve.html#codevalidator.format_syntax_error', 111 | 'arcsolver/solve.py'), 112 | 'arcsolver.solve.CodeValidator.validate': ('solve.html#codevalidator.validate', 'arcsolver/solve.py'), 113 | 'arcsolver.solve.ConcurrentExecutor': ('solve.html#concurrentexecutor', 'arcsolver/solve.py'), 114 | 'arcsolver.solve.ConcurrentExecutor.__init__': ( 'solve.html#concurrentexecutor.__init__', 115 | 'arcsolver/solve.py'), 116 | 'arcsolver.solve.ConcurrentExecutor.run_attempts': ( 'solve.html#concurrentexecutor.run_attempts', 117 | 'arcsolver/solve.py'), 118 | 'arcsolver.solve.ExecutionResult': ('solve.html#executionresult', 'arcsolver/solve.py'), 119 | 'arcsolver.solve.SandboxedExecutor': ('solve.html#sandboxedexecutor', 'arcsolver/solve.py'), 120 | 'arcsolver.solve.SandboxedExecutor.run': ('solve.html#sandboxedexecutor.run', 'arcsolver/solve.py'), 121 | 'arcsolver.solve.Solution': ('solve.html#solution', 'arcsolver/solve.py'), 122 | 'arcsolver.solve.Solution.from_response': ('solve.html#solution.from_response', 'arcsolver/solve.py'), 123 | 'arcsolver.solve.Solution.full_code': ('solve.html#solution.full_code', 'arcsolver/solve.py'), 124 | 'arcsolver.solve.SolutionTree': ('solve.html#solutiontree', 'arcsolver/solve.py'), 125 | 'arcsolver.solve.SolutionTree.__str__': ('solve.html#solutiontree.__str__', 'arcsolver/solve.py'), 126 | 'arcsolver.solve.SolutionTree._add_attempt_node': ( 'solve.html#solutiontree._add_attempt_node', 127 | 'arcsolver/solve.py'), 128 | 'arcsolver.solve.SolutionTree._collect_attempts': ( 'solve.html#solutiontree._collect_attempts', 129 | 'arcsolver/solve.py'), 130 | 'arcsolver.solve.SolutionTree.all_attempts': ( 'solve.html#solutiontree.all_attempts', 131 | 'arcsolver/solve.py'), 132 | 'arcsolver.solve.SolutionTree.best_attempt': ( 'solve.html#solutiontree.best_attempt', 133 | 'arcsolver/solve.py'), 134 | 'arcsolver.solve.SolutionTree.best_score': ('solve.html#solutiontree.best_score', 'arcsolver/solve.py'), 135 | 'arcsolver.solve.SolutionTree.correct': ('solve.html#solutiontree.correct', 'arcsolver/solve.py'), 136 | 'arcsolver.solve.SolutionTree.show': ('solve.html#solutiontree.show', 'arcsolver/solve.py'), 137 | 'arcsolver.solve.SolverProgress': ('solve.html#solverprogress', 'arcsolver/solve.py'), 138 | 'arcsolver.solve.ValidationError': ('solve.html#validationerror', 'arcsolver/solve.py'), 139 | 'arcsolver.solve._already_shown_task': ('solve.html#_already_shown_task', 'arcsolver/solve.py'), 140 | 'arcsolver.solve._image_message': ('solve.html#_image_message', 'arcsolver/solve.py'), 141 | 'arcsolver.solve._run_single_attempt': ('solve.html#_run_single_attempt', 'arcsolver/solve.py'), 142 | 'arcsolver.solve.attempt_solution': ('solve.html#attempt_solution', 'arcsolver/solve.py'), 143 | 'arcsolver.solve.clear_cache': ('solve.html#clear_cache', 'arcsolver/solve.py'), 144 | 'arcsolver.solve.feedback': ('solve.html#feedback', 'arcsolver/solve.py'), 145 | 'arcsolver.solve.retry_solution': ('solve.html#retry_solution', 'arcsolver/solve.py'), 146 | 'arcsolver.solve.run_solutions': ('solve.html#run_solutions', 'arcsolver/solve.py')}, 147 | 'arcsolver.task': { 'arcsolver.task.ArcGrid': ('task.html#arcgrid', 'arcsolver/task.py'), 148 | 'arcsolver.task.ArcGrid.__eq__': ('task.html#arcgrid.__eq__', 'arcsolver/task.py'), 149 | 'arcsolver.task.ArcGrid.__init__': ('task.html#arcgrid.__init__', 'arcsolver/task.py'), 150 | 'arcsolver.task.ArcGrid.__str__': ('task.html#arcgrid.__str__', 'arcsolver/task.py'), 151 | 'arcsolver.task.ArcGrid._draw_grid': ('task.html#arcgrid._draw_grid', 'arcsolver/task.py'), 152 | 'arcsolver.task.ArcGrid._setup_ax': ('task.html#arcgrid._setup_ax', 'arcsolver/task.py'), 153 | 'arcsolver.task.ArcGrid.plot': ('task.html#arcgrid.plot', 'arcsolver/task.py'), 154 | 'arcsolver.task.ArcPair': ('task.html#arcpair', 'arcsolver/task.py'), 155 | 'arcsolver.task.ArcPair.__getitem__': ('task.html#arcpair.__getitem__', 'arcsolver/task.py'), 156 | 'arcsolver.task.ArcPair.__init__': ('task.html#arcpair.__init__', 'arcsolver/task.py'), 157 | 'arcsolver.task.ArcPair.__iter__': ('task.html#arcpair.__iter__', 'arcsolver/task.py'), 158 | 'arcsolver.task.ArcPair.__len__': ('task.html#arcpair.__len__', 'arcsolver/task.py'), 159 | 'arcsolver.task.ArcPair.__str__': ('task.html#arcpair.__str__', 'arcsolver/task.py'), 160 | 'arcsolver.task.ArcPair._create_separator': ('task.html#arcpair._create_separator', 'arcsolver/task.py'), 161 | 'arcsolver.task.ArcPair.plot': ('task.html#arcpair.plot', 'arcsolver/task.py'), 162 | 'arcsolver.task.ArcTask': ('task.html#arctask', 'arcsolver/task.py'), 163 | 'arcsolver.task.ArcTask.__init__': ('task.html#arctask.__init__', 'arcsolver/task.py'), 164 | 'arcsolver.task.ArcTask.__str__': ('task.html#arctask.__str__', 'arcsolver/task.py'), 165 | 'arcsolver.task.ArcTask._load_data': ('task.html#arctask._load_data', 'arcsolver/task.py'), 166 | 'arcsolver.task.ArcTask.plot': ('task.html#arctask.plot', 'arcsolver/task.py'), 167 | 'arcsolver.task.get_task_files': ('task.html#get_task_files', 'arcsolver/task.py')}, 168 | 'arcsolver.utils': { 'arcsolver.utils.MultipleTagsError': ('utils.html#multipletagserror', 'arcsolver/utils.py'), 169 | 'arcsolver.utils.NoContentError': ('utils.html#nocontenterror', 'arcsolver/utils.py'), 170 | 'arcsolver.utils.TagNotFoundError': ('utils.html#tagnotfounderror', 'arcsolver/utils.py'), 171 | 'arcsolver.utils.parse_from_xml': ('utils.html#parse_from_xml', 'arcsolver/utils.py')}}} 172 | -------------------------------------------------------------------------------- /arcsolver/describe.py: -------------------------------------------------------------------------------- 1 | """Use Claude to analyse and describe a given ARC task""" 2 | 3 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_describe.ipynb. 4 | 5 | # %% auto 0 6 | __all__ = ['sp_direct', 'sp_indiv', 'sp_merge', 'Description', 'ShapeExtractor', 'DescriptionGenerator'] 7 | 8 | # %% ../nbs/02_describe.ipynb 5 9 | from .task import ArcTask, ArcPair 10 | from .ocm import Color 11 | from .utils import parse_from_xml 12 | from claudette import * 13 | from fastcore.utils import * 14 | from fastcore.meta import * 15 | from anthropic import AsyncAnthropic, AsyncAnthropicBedrock, AsyncAnthropicVertex 16 | from anthropic.types import Usage 17 | import asyncio 18 | import numpy as np 19 | from dataclasses import dataclass 20 | from typing import List, Dict, Optional 21 | from scipy import ndimage 22 | 23 | # %% ../nbs/02_describe.ipynb 7 24 | from toolslm.funccall import get_schema 25 | 26 | # %% ../nbs/02_describe.ipynb 8 27 | @patch 28 | async def __call__(self:AsyncChat, 29 | pr=None, # Prompt / message 30 | temp=0, # Temperature 31 | maxtok=4096, # Maximum tokens 32 | stream=False, # Stream response? 33 | prefill='', # Optional prefill to pass to Claude as start of its response 34 | **kw): 35 | await self._append_pr(pr) 36 | if self.tools: kw['tools'] = [get_schema(o) for o in self.tools] 37 | res = await self.c(self.h, stream=stream, prefill=prefill, sp=self.sp, temp=temp, maxtok=maxtok, **kw) 38 | if stream: return self._stream(res) 39 | self.h += mk_toolres(self.c.result, ns=self.tools) #, obj=self) 40 | return res 41 | 42 | # %% ../nbs/02_describe.ipynb 10 43 | sp_direct = """\ 44 | You are an expert at solving visual IQ puzzles involving transformations between input and output colored grids. \ 45 | Your task is to analyze images of puzzles and provide concise, accurate solutions. To solve a given puzzle, follow these steps: 46 | 47 | 1. INITIAL ANALYSIS 48 | a) Analyze the input and output grids carefully 49 | b) Note grid dimensions and any patterns in how they change 50 | c) Identify if there's a consistent background color 51 | d) Check if colors maintain consistent meanings across examples 52 | 53 | 2. ELEMENT IDENTIFICATION 54 | a) List all elements present in both input and output grids, counting them explicitly 55 | b) Note whether elements maintain their properties (color, size, shape) across examples 56 | c) Identify any hierarchical relationships between elements 57 | 58 | 3. TRANSFORMATION ANALYSIS 59 | a) Compare input and output grids side by side 60 | b) Note specific transformations for each element 61 | c) Analyze whether transformations apply: 62 | - Globally to the entire grid 63 | - To individual objects/regions 64 | - To specific color groups 65 | - To relationships between objects 66 | 67 | 4. PATTERN RECOGNITION 68 | a) Check for symmetry properties: 69 | - Rotational invariance 70 | - Mirror symmetry 71 | - Translation invariance 72 | - Scale invariance 73 | b) Look for pattern periodicity or repetition 74 | c) Analyze edge cases; e.g.: 75 | - Grid boundary interactions 76 | - Overlapping objects/patterns 77 | 78 | 5. CORE KNOWLEDGE PRIORS 79 | Bear in mind that this class of puzzles are solvable assuming only basic cognitive principles: 80 | a) Objectness: 81 | - Object cohesion 82 | - Object persistence 83 | - Influence via contact 84 | b) Goal-directedness (start and end states) 85 | c) Numbers and Counting (typically <10) 86 | d) Basic Geometry and Topology: 87 | - Lines, shapes, and basic geometric properties 88 | - Symmetries (rotational, mirror, translational) 89 | - Transformations (rotation, translation, scaling) 90 | - Spatial relationships and connectivity 91 | - Pattern periodicity and repetition 92 | e) Color consistency and meaning 93 | f) Hierarchical relationships 94 | 95 | 6. GRID PROPERTIES ANALYSIS 96 | a) Analyze dimension relationships: 97 | - Whether input/output dimensions are preserved 98 | - What determines output dimensions if different 99 | b) Consider how grid size affects transformation rules 100 | c) Look for patterns in provided dimension metadata 101 | 102 | 7. RULE FORMULATION 103 | a) Develop a general transformation rule 104 | b) Ensure the rule is: 105 | - Abstract and applicable to all examples 106 | - Deterministic (same input always produces same output) 107 | - Algorithmically implementable 108 | c) Consider alternative explanations and rule them out 109 | d) Test rule against edge cases 110 | 111 | 8. VALIDATION 112 | a) Double-check rule against all provided examples 113 | b) Verify rule consistency across different grid sizes 114 | c) Confirm rule handles all identified edge cases 115 | d) Refine rule if necessary 116 | 117 | Present your thought process in tags. \ 118 | This is where you can break down your observations, reasoning, and alternative explanations in detail. \ 119 | It's OK for this section to be quite long and to include explicit counting and detailed analysis. 120 | 121 | After your analysis, provide a concise solution summary in tags. This summary should: 122 | - Be no more than 4-5 sentences long 123 | - Clearly describe first (in generality) the properties/objects in the input grids 124 | - Then describe properties of the output grids and how an output grid is constructed from its input grid 125 | - Avoid instance-specific descriptions or if-else statements 126 | - Capture all key aspects of the transformation 127 | - Use precise, unambiguous language 128 | 129 | Your goal is to provide a clear, concise, and accurate description that captures the essence of the puzzle's \ 130 | transformation rule while being general enough to work across all examples. Remember to close xml tags. 131 | """ 132 | 133 | # %% ../nbs/02_describe.ipynb 13 134 | def _shape_table(pairs: List[ArcPair] # List of training example pairs 135 | ) -> str: # string containing a table of grid shapes 136 | header = "| Input Shape | Output Shape |\n|------------|-------------|\n" 137 | rows = "\n".join(f"| {str(i.shape):<10} | {str(o.shape):<11} |" for i, o in pairs) 138 | return header + rows 139 | 140 | # %% ../nbs/02_describe.ipynb 18 141 | @dataclass 142 | class Description: 143 | "A single description of an ARC task." 144 | content: str # The full description response (including reasoning) 145 | chats: List[AsyncChat] # Store all chats used to generate this description 146 | method: str # 'direct' or 'indirect' 147 | 148 | @property 149 | def d(self) -> str: 150 | "Extract just the description from the full response" 151 | return parse_from_xml(self.content, 'description') 152 | 153 | @property 154 | def usage(self) -> Usage: 155 | "Get combined token usage for this description." 156 | return sum((chat.use for chat in self.chats), start=usage()) 157 | 158 | @property 159 | def cost(self) -> float: 160 | "Get total cost in USD for this description." 161 | return sum(chat.cost for chat in self.chats) 162 | 163 | # %% ../nbs/02_describe.ipynb 20 164 | def _create_client(client_type, 165 | client_kwargs 166 | ) -> Union[AsyncAnthropic, AsyncAnthropicBedrock, AsyncAnthropicVertex]: 167 | "Create appropriate async client based on configuration." 168 | if client_type == "bedrock": 169 | return AsyncAnthropicBedrock(**client_kwargs) 170 | elif client_type == "vertex": 171 | return AsyncAnthropicVertex(**client_kwargs) 172 | else: # default to standard Anthropic 173 | return AsyncAnthropic( 174 | # default_headers={'anthropic-beta': 'prompt-caching-2024-07-31'}, 175 | **client_kwargs 176 | ) 177 | 178 | # %% ../nbs/02_describe.ipynb 21 179 | def _create_chat(model, client, sp: str, tools: Optional[list] = None) -> AsyncChat: 180 | "Create a new chat instance." 181 | cli = AsyncClient(model, client) 182 | return AsyncChat(cli=cli, sp=sp, tools=tools) 183 | 184 | # %% ../nbs/02_describe.ipynb 22 185 | async def _describe_direct( 186 | task: ArcTask | str, # Either an ArcTask object or a task ID string 187 | model: str = 'claude-3-5-sonnet-20241022', # Model identifier (defaults to Sonnet 3.5) 188 | client_type: str = 'anthropic', # 'anthropic', 'bedrock', or 'vertex' 189 | client_kwargs: Dict = {}, # Optional kwargs for client instantiation 190 | sp: str | None = None, # Custom system prompt (if None, uses `sp_direct`) 191 | temp: float = 0.0, # Sampling temperature for generation 192 | prefill: str = '', # Text to prefill the assistant's response with 193 | **kwargs # Additional arguments passed to Chat.__call__ 194 | ) -> Description: # Container holding description and the chat object 195 | "Generate a description of an ARC task from all examples at once" 196 | 197 | if sp is None: sp = sp_direct 198 | if isinstance(task, str): task = ArcTask(task) 199 | 200 | # Set up chat and get description 201 | client = _create_client(client_type, client_kwargs) 202 | chat = _create_chat(model, client, sp) 203 | 204 | pr = f"The example grids in the image have the following shapes:\n{_shape_table(task.train)}" 205 | in_cols = np.unique(np.hstack([p.input.data.flatten() for p in task.train])) 206 | out_cols = np.unique(np.hstack([p.output.data.flatten() for p in task.train])) 207 | pr += f"\nThe colors present in the input grids are: {', '.join(Color.colors[i] for i in in_cols)}\n" 208 | pr += f"\nThe colors present in the output grids are: {', '.join(Color.colors[i] for i in out_cols)}" 209 | 210 | r = await chat(mk_msg([task.plot(to_base64=True), pr], cache=client_type=='anthropic'), 211 | prefill=prefill, 212 | temp=temp, 213 | **kwargs) 214 | 215 | return Description( 216 | content=r.content[0].text, 217 | chats=[chat], 218 | method='direct' 219 | ) 220 | 221 | # %% ../nbs/02_describe.ipynb 25 222 | sp_indiv = """\ 223 | You are an expert puzzle analyst tasked with deciphering complex visual transformation puzzles. \ 224 | You analyze and describe the patterns and distinct shapes contained in input and output grids \ 225 | and judge what transformation maps input grids to output grids. 226 | 227 | Analysis Process: 228 | 1. INITIAL VISUAL ANALYSIS 229 | First, analyze the grids visually without tools: 230 | - Note obvious patterns, shapes, and colors 231 | - Form initial hypotheses about the transformation 232 | - Consider whether the transformation appears to be: 233 | * Global (affecting the entire grid) 234 | * Local (affecting specific shapes/regions) 235 | * Pattern-based (involving repetition or rules) 236 | - Think about why certain changes might occur 237 | 238 | 2. HYPOTHESIS REFINEMENT 239 | If your initial analysis leaves uncertainties, consider whether the shape extraction tool would help: 240 | - For complex or irregular shapes that might be related 241 | - To verify suspected rotations, reflections, repetitions, or scaling 242 | - To precisely analyze spatial relationships 243 | - To confirm pattern hypotheses 244 | Note that the shape extraction tool can only extract connected regions of a single color; often a "shape" in an ARC task is a distinct multi-colored region. 245 | 246 | Use the shape extraction tool judiciously to test specific hypotheses rather than as a first resort. Avoid using it when: 247 | - Shapes are simple and easily describable 248 | - The pattern is clearly global 249 | - The transformation is obvious visually 250 | - Colors appear to be background or noise rather than meaningful shapes 251 | 252 | Remember, these puzzles are designed to be solvable using only "Core Knowledge priors": 253 | 1. Objectness priors (object cohesion, persistence, and influence via contact) 254 | 2. Goal-directedness prior (conceptualize as intentional processes with start/end states) 255 | 3. Numbers and Counting priors (quantities typically <10) 256 | 4. Basic Geometry and Topology priors (lines, shapes, symmetries, rotations, translations, scaling, spatial relationships) 257 | 258 | Present your analysis in this format: 259 | 260 | 261 | Based on visual inspection: 262 | - Overview of grid properties and obvious patterns 263 | - Initial transformation hypothesis 264 | - Key uncertainties or questions 265 | - Whether tool analysis would be helpful and why 266 | 267 | 268 | If tool use is required, begin tool-calling process now. \ 269 | Once you have gathered the required information via tool calling, proceed with your analys as follows: 270 | 271 | 272 | Input: 273 | - Dimensions: [x, y] 274 | - Background: [color if applicable] 275 | - Shapes: [list major shapes with properties and positions] 276 | * Shape relationships: [note any rotational/reflectional/scaling relationships] 277 | * Spatial relationships: [relative positions, alignments, groupings] 278 | * Remember shapes can be multi-colored 279 | - Colors: [list with roles] 280 | - Notable patterns: [recurring elements, global patterns] 281 | 282 | Output: 283 | - [Same structure as Input] 284 | 285 | Transformations: 286 | - Size changes: [grid or shape scaling] 287 | - Shape changes: [rotations, reflections, splits, merges] 288 | - Color changes: [role changes, new colors, color relationships] 289 | - Position changes: [translations, relative position changes] 290 | - Pattern changes: [how global patterns transform] 291 | 292 | 293 | 294 | Provide your final transformation rule hypothesis, considering: 295 | - Whether the transformation is local or global 296 | - What properties are preserved 297 | - Why certain changes occur 298 | - How the transformation aligns with core knowledge priors 299 | - Any remaining uncertainties 300 | 301 | 302 | Note: It may not be possible to determine the exact relationship with full confidence. \ 303 | Make your best guess considering the points above, and note any uncertainties in your hypothesis. \ 304 | Remember to close xml tags. 305 | """ 306 | 307 | # %% ../nbs/02_describe.ipynb 27 308 | def _pair_prompt(pair: ArcPair, # A single training example pair from an ARC task 309 | example_idx: int = 0, # The index of the training example 310 | ): 311 | inp, outp = pair 312 | base_idx = example_idx * 2 313 | 314 | return f"""\ 315 | This image contains two grids from a visual IQ puzzle: 316 | 317 | input grid (left): 318 | - idx: {base_idx} 319 | - size: {inp.shape[0]}x{inp.shape[1]} 320 | - colors: {', '.join(Color.colors[i] for i in np.unique(inp.data))} 321 | 322 | output grid (right): 323 | - idx: {base_idx + 1} 324 | - size: {outp.shape[0]}x{outp.shape[1]} 325 | - colors: {', '.join(Color.colors[i] for i in np.unique(outp.data))}\ 326 | """ 327 | 328 | # %% ../nbs/02_describe.ipynb 30 329 | class ShapeExtractor: 330 | """Extract shapes from grid pairs for analysis.""" 331 | def __init__(self, task: ArcTask): 332 | # Store flattened list of grids in order 333 | self.grids = [grid for pair in task.train for grid in pair] 334 | 335 | def extract_shapes( 336 | self, 337 | grid_idx: int, # Index of the target grid 338 | color: str, # Color of shapes to extract 339 | include_diagonal: bool, # Consider diagonally adjacent cells as connected? 340 | ) -> list: # List of extracted shapes (boolean arrays) and their positions 341 | """Extract contiguous regions of a specified color from a grid.""" 342 | ORTH = np.array([[0,1,0], [1,1,1], [0,1,0]]) 343 | DIAG = np.ones((3,3)) 344 | 345 | try: 346 | arr = self.grids[grid_idx].data.copy() 347 | except IndexError as e: 348 | raise IndexError(f"Invalid grid_index {grid_idx}. Must be between 0 and {len(self.grids)-1}") from e 349 | 350 | value = Color.colors.index(color) 351 | mask = (arr == value) 352 | structure = DIAG if include_diagonal else ORTH 353 | labeled, _ = ndimage.label(mask, structure=structure) 354 | slices = ndimage.find_objects(labeled) 355 | 356 | regions = [[np.sum(mask[s]), s, labeled[s] == i+1] for i, s in enumerate(slices)] 357 | return [(r[2], (r[1][0].start, r[1][1].start)) for r in regions] 358 | 359 | # %% ../nbs/02_describe.ipynb 32 360 | @patch 361 | @delegates(AsyncChat.__call__) 362 | async def toolloop(self:AsyncChat, 363 | pr, # Prompt to pass to Claude 364 | max_steps=10, # Maximum number of tool requests to loop through 365 | trace_func:Optional[callable]=None, # Function to trace tool use steps (e.g `print`) 366 | cont_func:Optional[callable]=noop, # Function that stops loop if returns False 367 | **kwargs): 368 | "Add prompt `pr` to dialog and get a response from Claude, automatically following up with `tool_use` messages" 369 | n_msgs = len(self.h) - 1 370 | r = await self(pr, **kwargs) 371 | for i in range(max_steps): 372 | if r.stop_reason!='tool_use': break 373 | if trace_func: trace_func(self.h[n_msgs:]); n_msgs = len(self.h) - 1 374 | r = await self(**kwargs) 375 | if not (cont_func or noop)(self.h[-2]): break 376 | if trace_func: trace_func(self.h[n_msgs:]) 377 | return r 378 | 379 | # %% ../nbs/02_describe.ipynb 37 380 | sp_merge = """\ 381 | You are an expert puzzle analyst tasked with deciphering complex visual transformation puzzles. \ 382 | Your goal is to infer the general rule that governs how an input grid is transformed into an output grid \ 383 | based on multiple descriptions of individual grid pairs. 384 | 385 | The descriptions you'll analyze were generated by observers who each saw only one pair of grids. \ 386 | They had access to a shape extraction tool and were instructed to analyze shapes, patterns, and transformations in detail. 387 | 388 | Analysis Steps: 389 | 390 | 1. PATTERN IDENTIFICATION 391 | For each transformation aspect, analyze across all descriptions: 392 | - Grid size relationships 393 | - Shape transformations (rotations, reflections, scaling) 394 | - Color role patterns 395 | - Spatial relationship preservation 396 | - Global vs local patterns 397 | 398 | 2. CONSISTENCY ANALYSIS 399 | For each observed pattern, rate: 400 | - Frequency: How many descriptions support it? 401 | - Consistency: Are there any contradictions? 402 | - Completeness: Does it explain all observations? 403 | - Simplicity: Is it the simplest explanation? 404 | 405 | 3. TRANSFORMATION CLASSIFICATION 406 | Determine if transformations are: 407 | - Local (applying to individual shapes) 408 | - Global (applying to entire grid) 409 | - Hierarchical (different rules at different scales) 410 | - Composite (multiple transformations applied sequentially) 411 | 412 | 4. RELATIONSHIP ANALYSIS 413 | Look for patterns in: 414 | - How shapes relate to each other (rotation, reflection, scaling) 415 | - How shapes interact with grid boundaries 416 | - How colors relate to shapes and patterns 417 | - How relative positions are maintained or changed 418 | 419 | 5. EDGE CASE CONSIDERATION 420 | Consider how the rule would handle: 421 | - Overlapping/touching shapes 422 | - Shapes at grid boundaries 423 | - Any other relevant edge cases 424 | 425 | Show your analysis process in tags, including: 426 | 1. List of all unique characteristics across descriptions 427 | 2. Pattern frequency and consistency analysis 428 | 3. Alternative hypotheses considered 429 | 4. Evidence supporting your chosen rule 430 | 5. Potential edge cases and how they're handled 431 | 432 | After your analysis, provide a concise solution summary in tags. This summary should: 433 | - Be no more than 4-5 sentences long 434 | - Clearly describe first (in generality) the properties/objects in the input grids 435 | - Then describe properties of the output grids and how an output grid is constructed from its input grid 436 | - Avoid instance-specific descriptions or if-else statements 437 | - Capture all key aspects of the transformation 438 | - Use precise, unambiguous language, i.e. not use terms such as "follows a certain transformation", "according to some rule", etc. 439 | 440 | If multiple interpretations are possible, choose the simplest one that explains all observations while respecting these constraints. \ 441 | Remember to close xml tags. 442 | """ 443 | 444 | # %% ../nbs/02_describe.ipynb 38 445 | async def _describe_indirect( 446 | task: ArcTask | str, # Either an ArcTask object or a task ID string 447 | model: str = 'claude-3-5-sonnet-20241022', # Model identifier (defaults to Sonnet 3.5) 448 | client_type: str = 'anthropic', # 'anthropic', 'bedrock', or 'vertex' 449 | client_kwargs: Dict = {}, # Optional kwargs for client instantiation 450 | sp: str | None = None, # Custom system prompt for individual analysis (if None, uses `sp_indiv`) 451 | sp_combine: str | None = None, # Custom system prompt for synthesizing from independent descriptions (if None, uses `sp_combine`) 452 | temp: float = 0.0, # Sampling temperature for generation 453 | tools: Optional[list] = None, # List of tools to make available to Claude (defaults to `[ShapeExtractor.extract_shapes]`) 454 | **kwargs # Additional arguments passed to AsyncChat.__call__ 455 | ) -> Description: # Container holding description and the list of chats used 456 | "Generate a description of an ARC task by analyzing examples independently and then combining insights." 457 | 458 | if isinstance(task, str): task = ArcTask(task) 459 | 460 | # Use default prompts if none provided 461 | if sp is None: sp = sp_indiv 462 | if sp_combine is None: sp_combine = sp_merge 463 | # Create shape extractor 464 | if tools is None: 465 | extractor = ShapeExtractor(task) 466 | tools = [extractor.extract_shapes] 467 | 468 | # Create chats for each example pair 469 | pair_clients = [_create_client(client_type, client_kwargs) for _ in task.train] 470 | pair_chats = [_create_chat(model, c, sp, tools) for c in pair_clients] 471 | 472 | # Process examples concurrently 473 | pair_tasks = [ 474 | chat.toolloop(mk_msg([pair.plot(to_base64=True), _pair_prompt(pair, i)], cache=client_type=='anthropic'), temp=temp) 475 | for i, (chat, pair) in enumerate(zip(pair_chats, task.train)) 476 | ] 477 | responses = await asyncio.gather(*pair_tasks) 478 | 479 | # Format and merge descriptions 480 | merge_chat = _create_chat(model, _create_client(client_type, client_kwargs), sp_combine) 481 | descs = '\n\n'.join( 482 | f'\n{r.content[0].text}\n' 483 | for i, r in enumerate(responses) 484 | ) 485 | merged = await merge_chat([descs], temp=temp) 486 | 487 | # Create description with all chats used 488 | all_chats = pair_chats + [merge_chat] 489 | return Description( 490 | content=merged.content[0].text, 491 | chats=all_chats, 492 | method='indirect' 493 | ) 494 | 495 | # %% ../nbs/02_describe.ipynb 41 496 | class DescriptionGenerator: 497 | "Generates descriptions of ARC tasks using Claude." 498 | def __init__(self, 499 | model: str = "claude-3-5-sonnet-20241022", # Model identifier (defaults to Sonnet 3.5) 500 | client_type: str = "anthropic", # 'anthropic', 'bedrock', or 'vertex' 501 | client_kwargs: Optional[Dict] = None, # Optional kwargs for client instantiation 502 | direct_sp: Optional[str] = None, # Custom system prompt for direct description (if None, uses `sp_direct`) 503 | indirect_sp: Optional[str] = None, # Custom system prompt for single pair description (if None, uses `sp_indiv`) 504 | merge_sp: Optional[str] = None): # Custom system prompt for synthesized description (if None, uses `sp_merge`) 505 | self.model = model 506 | self.client_type = client_type 507 | self.client_kwargs = client_kwargs or {} 508 | self.direct_sp = direct_sp or sp_direct 509 | self.indirect_sp = indirect_sp or sp_indiv 510 | self.merge_sp = merge_sp or sp_merge 511 | 512 | def _create_client(self) -> Union[AsyncAnthropic, AsyncAnthropicBedrock, AsyncAnthropicVertex]: 513 | "Create appropriate async client based on configuration." 514 | return _create_client(self.client_type, self.client_kwargs) 515 | 516 | def _create_chat(self, sp: str, tools: Optional[list] = None) -> AsyncChat: 517 | "Create a new chat instance." 518 | return _create_chat(self.model, self._create_client(), sp, tools) 519 | 520 | # %% ../nbs/02_describe.ipynb 44 521 | @patch 522 | async def describe_direct( 523 | self: DescriptionGenerator, 524 | task: ArcTask | str, # ARC task or task ID to describe 525 | n: int = 1, # No. of descriptions to generate 526 | temp: float = 0.5, # Temperature for generation (higher for diversity) 527 | prefill: str = '', # Text to prefill the assistant's response with 528 | **kwargs # Additional arguments passed to AsyncChat.__call__ 529 | ) -> List[Description]: # List of `Description` objects 530 | "Generate n direct descriptions of the task concurrently." 531 | tasks = [ 532 | _describe_direct(task, self.model, self.client_type, self.client_kwargs, self.direct_sp, temp, prefill, **kwargs) 533 | for _ in range(n) 534 | ] 535 | return await asyncio.gather(*tasks) 536 | 537 | # %% ../nbs/02_describe.ipynb 53 538 | @patch 539 | async def describe_indirect( 540 | self: DescriptionGenerator, 541 | task: ArcTask | str, # ARC task or task ID to describe 542 | n: int = 1, # No. of descriptions to generate 543 | temp: float = 0.6, # Temperature for generation (higher for diversity) 544 | tools: Optional[list] = None, # List of tools to make available to Claude (defaults to `[ShapeExtractor.extract_shapes]`) 545 | **kwargs # Additional arguments passed to AsyncChat.__call__ 546 | ) -> List[Description]: # List of `Description` objects 547 | "Generate n direct descriptions of the task concurrently." 548 | tasks = [ 549 | _describe_indirect(task, self.model, self.client_type, self.client_kwargs, self.indirect_sp, self.merge_sp, temp, tools, **kwargs) 550 | for _ in range(n) 551 | ] 552 | return await asyncio.gather(*tasks) 553 | 554 | # %% ../nbs/02_describe.ipynb 64 555 | @patch 556 | async def describe_task( 557 | self: DescriptionGenerator, 558 | task: ArcTask | str, # ARC task or task ID to describe 559 | n_direct: int = 1, # No. of direct descriptions to generate 560 | n_indirect: int = 1, # No. of indirect descriptions to generate 561 | temp: float = 0.7, # Temperature for generation (higher for diversity) 562 | **kwargs 563 | ) -> List[Description]: # List of `Description` objects 564 | "Generate multiple descriptions of a task using one or both strategies concurrently." 565 | 566 | # Generate all descriptions concurrently 567 | descriptions = await asyncio.gather( 568 | self.describe_direct(task, n=n_direct, temp=temp, prefill=None, **kwargs), 569 | self.describe_indirect(task, n=n_indirect, temp=temp, tools=None, **kwargs) 570 | ) 571 | 572 | return [d for d_list in descriptions for d in d_list] 573 | -------------------------------------------------------------------------------- /arcsolver/examples.py: -------------------------------------------------------------------------------- 1 | """Hand-written example ARC solutions to be used in solver""" 2 | 3 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_examples.ipynb. 4 | 5 | # %% auto 0 6 | __all__ = ['example_017c7c7b', 'example_36d67576', 'example_007bbfb7', 'examples', 'Example'] 7 | 8 | # %% ../nbs/05_examples.ipynb 4 9 | from .task import * 10 | from dataclasses import dataclass, asdict 11 | 12 | # %% ../nbs/05_examples.ipynb 5 13 | @dataclass 14 | class Example: 15 | "Represents an example ARC solution" 16 | description: str # Task description 17 | reasoning: str # Reasoning process to generate solution code 18 | new_primitives: str # Proposed new primitive classes 19 | input_model: str # Input grid model 20 | output_model: str # Output grid model 21 | 22 | dict = asdict 23 | 24 | # %% ../nbs/05_examples.ipynb 8 25 | example_017c7c7b = Example( 26 | description="""\ 27 | The input grid is a 6x3 grid with a blue colored pattern on a black background. \ 28 | The output grid is a 9x3 grid. The first 6 rows of the output grid contain the same pattern as the input grid \ 29 | and the additional rows are filled appropriately to form a cyclical or repeating pattern of rows. \ 30 | The resulting cyclical patterns in the output grids have varying cycle lengths. \ 31 | The color of the entire pattern in the output grid is changed from blue to red. 32 | """, 33 | reasoning="""\ 34 | 1. Pattern & Transformation Analysis: 35 | - Fixed-size grids: 6x3 input, 9x3 output 36 | - Single blue pattern on black background transforms to red 37 | - Pattern extends vertically with cyclic repetition 38 | - Cycle length varies between examples 39 | - Two key transformations: cyclic extension and color change (blue → red) 40 | 41 | 2. Shape Extraction & Representation: 42 | - Entire grid is one cohesive pattern 43 | - No need for complex shape extraction 44 | - Simple Bitmap representation sufficient 45 | - Background color consistent (black) 46 | 47 | 3. Required Capabilities: 48 | - Period detection for cyclic patterns 49 | - Pattern extension while preserving structure 50 | - Color transformation (value mapping 1 → 2) 51 | - No existing primitive handles cyclic patterns 52 | 53 | 4. Implementation Strategy: 54 | - Input: Single Bitmap object preserving full grid 55 | - Output: Detect period, extend pattern, transform color 56 | - New CyclicPattern primitive needed for: 57 | * Period detection along specified axis 58 | * Pattern extension to desired length 59 | * Integration with existing primitives 60 | 61 | 5. Edge Cases: 62 | - Various cycle lengths (1, 2, 3...) 63 | - Patterns without obvious repetition 64 | - Full-length cycles 65 | 66 | This suggests a solution focusing on pattern-level operations rather than individual shapes, \ 67 | with a new CyclicPattern primitive handling the cyclic nature of the transformations. \ 68 | The implementation can be straightforward since we're treating each grid as a single pattern, \ 69 | with complexity mainly in period detection and extension. 70 | """, 71 | new_primitives='''\ 72 | class CyclicPattern(BaseModel): 73 | """ 74 | Identify, represent, and manipulate cyclic patterns in ARC task grids, 75 | particularly for tasks involving pattern repetition and extension. 76 | """ 77 | data: np.ndarray 78 | period: int 79 | axis: int = Field(0, ge=0, le=1) 80 | 81 | model_config = {"arbitrary_types_allowed": True} 82 | 83 | @classmesthod 84 | def from_array(cls, arr: np.ndarray, axis: int = 0) -> 'CyclicPattern': 85 | """Create a CyclicPattern instance from a numpy array.""" 86 | return cls(data=arr, period=cls.find_period(arr, axis), axis=axis) 87 | 88 | @staticmethod 89 | def find_period(arr: np.ndarray, axis: int = 0) -> int: 90 | """Find the smallest period along a specified axis of a NumPy array.""" 91 | n = arr.shape[axis] 92 | if n == 0: return 0 # Undefined period for empty axis 93 | 94 | for p in range(1, n): 95 | pattern = np.take(arr, indices=range(p), axis=axis) 96 | repeats = int(np.ceil(n / p)) 97 | tiled = np.concatenate([pattern] * repeats, axis=axis) 98 | slicer = [slice(None)] * arr.ndim 99 | slicer[axis] = slice(0, n) 100 | tiled = tiled[tuple(slicer)] 101 | 102 | if np.array_equal(arr, tiled): return p 103 | 104 | return n # The entire axis if no smaller period is found 105 | 106 | def extend(self, length: int) -> np.ndarray: 107 | """Extend the pattern to a specified length.""" 108 | pattern = np.take(self.data, range(self.period), axis=self.axis) 109 | repeats = [1] * self.data.ndim 110 | repeats[self.axis] = length // self.period + 1 111 | tiled = np.tile(pattern, repeats) 112 | slices = [slice(None)] * self.data.ndim 113 | slices[self.axis] = slice(length) 114 | return tiled[tuple(slices)] 115 | 116 | def to_array(self) -> np.ndarray: 117 | """Convert the cyclic pattern to a numpy array.""" 118 | return self.data 119 | 120 | def __len__(self) -> int: 121 | """Get the length of the pattern along its axis.""" 122 | return self.data.shape[self.axis]\ 123 | ''', 124 | input_model='''\ 125 | class InputModel(Grid): 126 | size: Literal[Vector(6, 3)] 127 | 128 | @classmethod 129 | def from_array(cls, arr: np.ndarray) -> 'InputModel': 130 | return cls( 131 | size=Vector(*arr.shape), 132 | objects=[Bitmap(position=Vector(0, 0), data=arr)] 133 | )\ 134 | ''', 135 | output_model='''\ 136 | class OutputModel(Grid): 137 | size: Literal[Vector(9, 3)] 138 | 139 | @classmethod 140 | def from_input(cls, input_grid: InputModel) -> 'OutputModel': 141 | input_pattern = CyclicPattern.from_array(input_grid.objects[0].data, axis=0) 142 | extended_pattern = input_pattern.extend(9) 143 | red_pattern = np.where(extended_pattern == 1, 2, 0) 144 | 145 | return cls( 146 | size=Vector(9,3), 147 | objects=[Bitmap(position=Vector(0, 0), data=red_pattern)] 148 | )\ 149 | ''' 150 | ) 151 | 152 | # %% ../nbs/05_examples.ipynb 11 153 | example_36d67576 = Example( 154 | description='''\ 155 | The input grid contains multiple shapes: one complete shape and several partial copies of that shape. \ 156 | The partial copies are transformed versions of the complete shape (rotated and/or reflected). \ 157 | The task is to identify how each partial shape relates to the complete shape and replace each partial shape \ 158 | with its complete transformed version.\ 159 | ''', 160 | reasoning='''\ 161 | 1. Task Analysis: 162 | - Input contains multiple shapes: 163 | * One complete reference shape 164 | * Multiple partial shapes 165 | - Partial shapes are transformed versions of complete shape 166 | - Transformations include rotations and reflections 167 | - Goal: Replace partials with complete transformed versions 168 | 169 | 2. Shape Identification Requirements: 170 | - Need to extract contiguous shapes from grid 171 | - Must distinguish complete from partial shapes 172 | - First shape extracted is likely complete (largest) 173 | - ShapeExtractor primitive suitable for this task 174 | 175 | 3. Transformation Analysis: 176 | - Need to test all possible transformations: 177 | * Rotations (0°, 90°, 180°, 270°) 178 | * Reflections (horizontal, vertical) 179 | * Transpositions 180 | - Must match partial shapes allowing for missing parts 181 | - PatternMatcher with 'allow_extra' mode ideal for this 182 | 183 | 4. Implementation Strategy: 184 | - Input Model: 185 | * Separate complete shape from partials 186 | * Store as Bitmap objects 187 | * Use background_color for grid reconstruction 188 | - Output Model: 189 | * Generate all possible transformations 190 | * Find best match for each partial shape 191 | * Replace partials with complete transformed versions 192 | 193 | 5. Edge Cases: 194 | - Multiple equally-sized shapes 195 | - Partial shapes with minimal overlap 196 | - Ambiguous transformations 197 | - Different colored shapes 198 | 199 | No new primitives needed as existing ones (ShapeExtractor, PatternMatcher, Bitmap) 200 | provide all required functionality for shape extraction, transformation matching, 201 | and grid reconstruction.\ 202 | ''', 203 | new_primitives='', 204 | input_model='''\ 205 | class InputModel(Grid): 206 | background_color: Literal[Color(0)] = Color(0) 207 | full_shape: Bitmap 208 | partial_shapes: List[Bitmap] 209 | 210 | @model_validator(mode='before') 211 | def set_objects(cls, values): 212 | values['objects'] = [values['full_shape']] + values['partial_shapes'] 213 | return values 214 | 215 | @classmethod 216 | def from_array(cls, arr: np.ndarray) -> 'InputModel': 217 | arr_mask = (arr != 0).astype(int) 218 | shape_info = ShapeExtractor.extract_contiguous_regions(arr_mask, 1) 219 | colored_shapes = [] 220 | for shape, pos in shape_info: 221 | colored_shapes.append(( 222 | arr[pos[0]:pos[0]+shape.shape[0], pos[1]:pos[1]+shape.shape[1]], 223 | pos 224 | )) 225 | 226 | full_shape = Bitmap(position=Vector(*colored_shapes[0][1]), 227 | data=colored_shapes[0][0]) 228 | partial_shapes = [ 229 | Bitmap(position=Vector(*pos), data=shape) for shape, pos in colored_shapes[1:] 230 | ] 231 | 232 | return cls( 233 | size=Vector(*arr.shape), 234 | full_shape=full_shape, 235 | partial_shapes=partial_shapes 236 | )\ 237 | ''', 238 | output_model='''\ 239 | class OutputModel(Grid): 240 | @classmethod 241 | def from_input(cls, input_grid: InputModel) -> 'OutputModel': 242 | full_shape = input_grid.full_shape.to_array() 243 | transformations = [ 244 | full_shape, 245 | np.rot90(full_shape, 1), 246 | np.rot90(full_shape, 2), 247 | np.rot90(full_shape, 3), 248 | np.flip(full_shape, 0), 249 | np.flip(full_shape, 1), 250 | full_shape.T, 251 | np.fliplr(full_shape).T 252 | ] 253 | 254 | new_shapes = [] 255 | for obj in input_grid.partial_shapes: 256 | # Create a grid with just the partial shape object 257 | obj_arr = obj.to_array(input_grid.size) 258 | obj_arr[obj_arr == -1] = 0 259 | # Find the transformation that best matches the partial shape 260 | matches = [(t, *PatternMatcher.find_best_match(obj_arr, t, match_type='allow_extra')) for t in transformations] 261 | best_match = max(matches, key=lambda m: m[-1]) # matches is a list of (transformation, position, overlap_count) 262 | new_shapes.append(Bitmap(position=best_match[1], data=best_match[0])) 263 | 264 | return cls( 265 | size=input_grid.size, 266 | background_color=input_grid.background_color, 267 | objects=[input_grid.full_shape] + new_shapes 268 | )\ 269 | ''' 270 | ) 271 | 272 | # %% ../nbs/05_examples.ipynb 14 273 | example_007bbfb7 = Example( 274 | description='''\ 275 | The input grid is a 3x3 grid with a single-colored pattern on a black background. \ 276 | The output grid size is 9x9. Each pixel in the input grid corresponds to a 3x3 section in the output grid. \ 277 | For each pixel in the input grid that is colored, its corresponding section of the output grid is filled \ 278 | with a copy of the entire input grid.\ 279 | ''', 280 | reasoning='''\ 281 | 1. Pattern Analysis: 282 | - Input: 3x3 grid with single-colored pattern on black 283 | - Output: 9x9 grid with recursive pattern structure 284 | - Each colored pixel in input maps to 3x3 section in output 285 | - These 3x3 sections contain copies of entire input pattern 286 | 287 | 2. Transformation Properties: 288 | - Scale factor is 3 (3x3 → 9x9) 289 | - Pattern replication follows a specific rule: 290 | * For each non-zero input pixel 291 | * Replace with full input pattern 292 | * For zero pixels, keep as zero 293 | - This resembles a Kronecker product operation: 294 | * Binary mask of pattern × Original pattern 295 | * Will naturally create the required structure 296 | 297 | 3. Implementation Strategy: 298 | - Input Model: 299 | * Single Bitmap sufficient for 3x3 pattern 300 | * Fixed size constraint (3x3) 301 | - Output Model: 302 | * Create binary mask of non-zero elements 303 | * Apply Kronecker product for pattern replication 304 | * Result gives correct 9x9 structure 305 | 306 | 4. Edge Cases: 307 | - Empty input patterns 308 | - Single pixel patterns 309 | - Fully filled patterns 310 | - Different colors 311 | 312 | No new primitives needed as the transformation can be handled efficiently using 313 | numpy's Kronecker product (kron) operation combined with existing Bitmap primitive.\ 314 | ''', 315 | new_primitives='', 316 | input_model='''\ 317 | class InputModel(Grid): 318 | size: Literal[Vector(3, 3)] 319 | pattern: Bitmap 320 | 321 | @model_validator(mode='before') 322 | def set_objects(cls, values): 323 | values['objects'] = [values['pattern']] 324 | return values 325 | 326 | @classmethod 327 | def from_array(cls, arr: np.ndarray) -> 'InputModel': 328 | return cls( 329 | size=Vector(*arr.shape), 330 | pattern=Bitmap(position=Vector(i=0, j=0), data=arr) 331 | )\ 332 | ''', 333 | output_model='''\ 334 | class OutputModel(Grid): 335 | size: Literal[Vector(9, 9)] = Vector(9, 9) 336 | 337 | @classmethod 338 | def from_input(cls, input_grid: InputModel) -> 'OutputModel': 339 | input_pattern = input_grid.pattern.data 340 | binary_mask = (input_pattern != 0).astype(int) 341 | output_pattern = np.kron(binary_mask, input_pattern) 342 | 343 | return cls( 344 | objects=[Bitmap(position=Vector(0, 0), data=output_pattern)] 345 | )\ 346 | ''' 347 | ) 348 | 349 | # %% ../nbs/05_examples.ipynb 15 350 | examples = [example_017c7c7b, example_36d67576, example_007bbfb7] 351 | -------------------------------------------------------------------------------- /arcsolver/ocm.py: -------------------------------------------------------------------------------- 1 | """Primitive classes for constructing object-centric models (OCMs) for ARC tasks""" 2 | 3 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_ocm.ipynb. 4 | 5 | # %% auto 0 6 | __all__ = ['Vector', 'Color', 'Direction', 'Object', 'Rectangle', 'Line', 'Bitmap', 'Grid', 'ShapeExtractor', 'PatternMatcher', 7 | 'EnclosureFiller', 'CyclicPattern'] 8 | 9 | # %% ../nbs/01_ocm.ipynb 5 10 | from fastcore.utils import * 11 | from enum import Enum 12 | from typing import ClassVar, List, Optional, Tuple, Union, Literal 13 | import numpy as np 14 | from pydantic import BaseModel, Field, field_validator, model_validator 15 | from scipy import ndimage 16 | 17 | # %% ../nbs/01_ocm.ipynb 8 18 | class Vector(BaseModel): 19 | "2D vector for positions, sizes, and directions." 20 | i: int 21 | j: int 22 | 23 | def __init__(self, i: int, j: int): 24 | super().__init__(i=i, j=j) 25 | 26 | def to_array(self) -> np.ndarray: 27 | return np.array([self.i, self.j], dtype=int) 28 | 29 | @classmethod 30 | def from_array(cls, a: Union[np.ndarray, List, Tuple]) -> 'Vector': 31 | if not isinstance(a, np.ndarray): a = np.array(a) 32 | if a.shape != (2,) or a.dtype != int: 33 | raise ValueError("Need 1D array with 2 ints") 34 | return cls(i=int(a[0]), j=int(a[1])) 35 | 36 | def __add__(self, o): return Vector(i=self.i + o.i, j=self.j + o.j) 37 | def __sub__(self, o): return Vector(i=self.i - o.i, j=self.j - o.j) 38 | def __mul__(self, s): return Vector(i=self.i * s, j=self.j * s) 39 | def __rmul__(self, s): return self.__mul__(s) 40 | 41 | # %% ../nbs/01_ocm.ipynb 17 42 | class Color(BaseModel): 43 | "Represents a color using an integer value (0-9) or a color name." 44 | value: int = Field(..., ge=0, le=9) 45 | colors: ClassVar[List[str]] = ['black', 'blue', 'red', 'green', 'yellow', 'grey', 'pink', 'orange', 'cyan', 'brown'] 46 | 47 | def __init__(self, value: Union[int, str]): 48 | if isinstance(value, str): value = self.colors.index(value.lower()) 49 | super().__init__(value=value) 50 | 51 | def to_array(self): return np.array([self.value], dtype=int) 52 | 53 | def __str__(self): return f"Color('{self.colors[self.value]}', value={self.value})" 54 | __repr__ = __str__ 55 | 56 | # %% ../nbs/01_ocm.ipynb 23 57 | class Direction(Enum): 58 | "Represents cardinal and intercardinal directions as 2D vectors." 59 | UP = Vector(i=-1, j=0) 60 | DOWN = Vector(i=1, j=0) 61 | LEFT = Vector(i=0, j=-1) 62 | RIGHT = Vector(i=0, j=1) 63 | NE = Vector(i=-1, j=1) 64 | SW = Vector(i=1, j=-1) 65 | NW = Vector(i=-1, j=-1) 66 | SE = Vector(i=1, j=1) 67 | 68 | def to_array(self): return self.value.to_array() 69 | 70 | @classmethod 71 | def from_array(cls, arr: np.ndarray) -> 'Direction': 72 | v = Vector(*arr) 73 | for dir in cls: 74 | if dir.value == v: return dir 75 | raise ValueError(f"No direction matches {v}") 76 | 77 | # %% ../nbs/01_ocm.ipynb 28 78 | class Object(BaseModel): 79 | "Base class for shape objects in ARC tasks." 80 | position: Vector 81 | 82 | def _get_shape_array(self) -> np.ndarray: 83 | "Abstract method for getting the shape's array without positioning." 84 | raise NotImplementedError("Subclasses must implement this method") 85 | 86 | # %% ../nbs/01_ocm.ipynb 29 87 | class Rectangle(Object): 88 | "Rectangular shape." 89 | size: Vector 90 | color: Color 91 | 92 | def _get_shape_array(self) -> np.ndarray: 93 | return np.full(self.size.to_array(), self.color.value, dtype=int) 94 | 95 | # %% ../nbs/01_ocm.ipynb 33 96 | class Line(Object): 97 | "Line shape." 98 | direction: Direction 99 | length: int = Field(..., gt=0) 100 | color: Color 101 | 102 | def _get_shape_array(self) -> np.ndarray: 103 | if self.direction in [Direction.UP, Direction.DOWN]: shape = (self.length, 1) 104 | elif self.direction in [Direction.LEFT, Direction.RIGHT]: shape = (1, self.length) 105 | else: shape = (self.length, self.length) 106 | 107 | arr = np.full(shape, -1) 108 | if self.direction in [Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT]: 109 | arr.fill(self.color.value) 110 | elif self.direction in [Direction.NE, Direction.SW]: 111 | np.fill_diagonal(np.fliplr(arr), self.color.value) 112 | else: # NW, SE 113 | np.fill_diagonal(arr, self.color.value) 114 | return arr 115 | 116 | # %% ../nbs/01_ocm.ipynb 38 117 | @patch(as_prop=True) 118 | def offset(self: Line) -> Vector: 119 | "Calculate the offset of the line based on its direction." 120 | if self.direction == Direction.UP: return Vector(-self.length + 1, 0) 121 | elif self.direction == Direction.LEFT: return Vector(0, -self.length + 1) 122 | elif self.direction == Direction.NW: return Vector(-self.length + 1, -self.length + 1) 123 | elif self.direction == Direction.NE: return Vector(-self.length + 1, 0) 124 | elif self.direction == Direction.SW: return Vector(0, -self.length + 1) 125 | return Vector(0, 0) 126 | 127 | # %% ../nbs/01_ocm.ipynb 40 128 | class Bitmap(Object): 129 | "Multi-colored bitmap pattern." 130 | data: np.ndarray 131 | 132 | model_config = {"arbitrary_types_allowed": True} 133 | 134 | @field_validator('data') 135 | @classmethod 136 | def validate_data(cls, v): 137 | if not isinstance(v, np.ndarray): raise ValueError('Must be a numpy array') 138 | if v.ndim != 2: raise ValueError('Must be a 2D array') 139 | if v.dtype != int: raise ValueError('Array must contain integer values') 140 | if np.any(v < -1) or np.any(v > 9): raise ValueError('Array values must be between -1 and 9') 141 | return v 142 | 143 | def _get_shape_array(self) -> np.ndarray: return self.data 144 | 145 | @property 146 | def size(self) -> Vector: return Vector.from_array(self.data.shape) 147 | 148 | def rotate(self, n=1): return Bitmap(position=self.position, data=np.rot90(self.data, k=n)) 149 | def flip(self, axis): return Bitmap(position=self.position, data=np.flip(self.data, axis=axis)) 150 | 151 | # %% ../nbs/01_ocm.ipynb 44 152 | @patch 153 | def to_array(self: Object, grid_size: Optional[Vector] = None) -> np.ndarray: 154 | """ 155 | If grid_size is provided, returns the object positioned within a grid of that size. 156 | If grid_size is None, returns just the object's array. 157 | """ 158 | obj_arr = self._get_shape_array() 159 | if not grid_size: return obj_arr 160 | 161 | full = np.full(grid_size.to_array(), -1, dtype=int) 162 | offset = self.offset if isinstance(self, Line) else Vector(i=0, j=0) 163 | obj_pos = self.position + offset 164 | 165 | if obj_pos.i >= grid_size.i or obj_pos.j >= grid_size.j: return full 166 | 167 | # Allow negative positions to crop the object 168 | if obj_pos.i < 0: 169 | obj_arr = obj_arr[-obj_pos.i:] 170 | obj_pos.i = 0 171 | if obj_pos.j < 0: 172 | obj_arr = obj_arr[:, -obj_pos.j:] 173 | obj_pos.j = 0 174 | 175 | # Crop the object if it goes beyond the grid 176 | obj_arr = obj_arr[:min(grid_size.i - obj_pos.i, obj_arr.shape[0]), 177 | :min(grid_size.j - obj_pos.j, obj_arr.shape[1])] 178 | 179 | full[obj_pos.i:obj_pos.i + obj_arr.shape[0], 180 | obj_pos.j:obj_pos.j + obj_arr.shape[1]] = obj_arr 181 | return full 182 | 183 | # %% ../nbs/01_ocm.ipynb 51 184 | class Grid(BaseModel): 185 | "Grid container with a size, background color, and objects." 186 | size: Vector # Size of the grid (r x c) 187 | background_color: Optional[Color] = None # Must be provided if the object layers do not cover the entire grid. 188 | objects: List[Object] = Field(default_factory=list) # List of objects contained in the grid. 189 | 190 | def to_array(self) -> np.ndarray: 191 | grid = self._create_grid(self.size, self.objects) 192 | # Only replace -1 values with background color if it exists 193 | if self.background_color is not None: 194 | grid = np.where(grid == -1, self.background_color.value, grid) 195 | return grid 196 | 197 | def _create_grid(self, size: Vector, objects: List[Object]) -> np.ndarray: 198 | "Create grid array without using background color." 199 | grid = np.full(size.to_array(), -1, dtype=int) 200 | for obj in objects: 201 | obj_array = obj.to_array(size) 202 | if self.background_color is not None: 203 | obj_array[obj_array == self.background_color.value] = -1 204 | grid = np.where(obj_array != -1, obj_array, grid) 205 | return grid 206 | 207 | @model_validator(mode='after') 208 | def validate_grid_coverage(self) -> 'Grid': 209 | if self.background_color is None: 210 | grid = self._create_grid(self.size, self.objects) 211 | if np.any(grid == -1): 212 | raise ValueError("When background_color is not provided, objects must cover the entire grid") 213 | return self 214 | 215 | # %% ../nbs/01_ocm.ipynb 73 216 | class ShapeExtractor: 217 | 'Extract distinct "shapes" (i.e. contiguous regions of the same value) from a numpy array' 218 | # Define the connectivity structures as class attributes 219 | ORTH = np.array([[0,1,0], [1,1,1], [0,1,0]]) 220 | DIAG = np.ones((3,3)) 221 | 222 | @staticmethod 223 | def extract_contiguous_regions( 224 | array: np.ndarray, 225 | value: int, 226 | include_diagonal: bool = False, 227 | background_color: Optional[int] = None 228 | ) -> List[Tuple[np.ndarray, Tuple[int, int]]]: 229 | """Extract contiguous regions of a specified value from a numpy array. Can include diagonal connections if specified.""" 230 | arr = array.copy() 231 | mask = (arr == value) 232 | structure = ShapeExtractor.DIAG if include_diagonal else ShapeExtractor.ORTH 233 | labeled, _ = ndimage.label(mask, structure=structure) 234 | slices = ndimage.find_objects(labeled) 235 | 236 | regions = sorted([(np.sum(mask[s]), s, arr[s]) for s in slices], 237 | key=lambda x: x[0], reverse=True) 238 | for region in regions: 239 | if background_color is not None: region[2][region[2] == background_color] = -1 240 | 241 | return [(r[2], (r[1][0].start, r[1][1].start)) for r in regions] 242 | 243 | @staticmethod 244 | def extract_largest_shape( 245 | array: np.ndarray, 246 | value: int, 247 | include_diagonal: bool = False, 248 | background_color: Optional[int] = None 249 | ) -> Tuple[Optional[np.ndarray], Optional[Tuple[int, int]]]: 250 | """ 251 | Extract the largest contiguous shape of a specified value from a numpy array.""" 252 | regions = ShapeExtractor.extract_contiguous_regions(array, value, include_diagonal, background_color) 253 | return regions[0] if regions else (None, None) 254 | 255 | @staticmethod 256 | def extract_all_shapes( 257 | array: np.ndarray, # Numpy array 258 | include_diagonal: bool = False, # Consider diagonally adjacent cells as connected or not 259 | background_color: Optional[int] = None # Optionally specify a background color to ignore 260 | ) -> List[Tuple[np.ndarray, Tuple[int, int], int]]: # List of (sub-array, position, color_value) tuples 261 | "Extract all shapes of all values from a numpy array." 262 | all_shapes = [] 263 | for value in np.unique(array): 264 | if background_color is not None and value == background_color: continue 265 | shapes = ShapeExtractor.extract_contiguous_regions(array, value, include_diagonal, background_color) 266 | all_shapes.extend([(shape, pos, value) for shape, pos in shapes]) 267 | return all_shapes 268 | 269 | # %% ../nbs/01_ocm.ipynb 76 270 | class PatternMatcher: 271 | """ 272 | A class for finding alignments between patterns in numpy arrays. 273 | Supports exact matching and partial matching with missing or extra elements. 274 | """ 275 | 276 | @staticmethod 277 | def find_matches( 278 | target: np.ndarray, # The larger array to search in 279 | pattern: np.ndarray, # The smaller array containing the pattern to match 280 | match_type: str = 'exact' # 'exact', 'allow_missing', or 'allow_extra' 281 | ) -> List[Tuple[Vector, int]]: 282 | """ 283 | Find positions where a pattern aligns with a target, sorted by overlap count 284 | for non-exact matches. 285 | """ 286 | h, w = pattern.shape 287 | H, W = target.shape 288 | matches = [] 289 | 290 | for i in range(H - h + 1): 291 | for j in range(W - w + 1): 292 | window = target[i:i+h, j:j+w] 293 | 294 | # Get masks for non-zero elements 295 | pattern_nonzero = pattern != 0 296 | window_nonzero = window != 0 297 | 298 | # Check for mismatches based on match_type 299 | if match_type != 'allow_missing': 300 | if np.any(window_nonzero & ~pattern_nonzero): continue 301 | 302 | if match_type != 'allow_extra': 303 | if np.any(pattern_nonzero & ~window_nonzero): continue 304 | 305 | # Check that overlapping non-zero elements match 306 | both_nonzero = window_nonzero & pattern_nonzero 307 | if not np.array_equal(window[both_nonzero], pattern[both_nonzero]): continue 308 | 309 | # Count overlapping non-zero elements and only include non-zero overlaps 310 | overlap_count = np.sum(both_nonzero) 311 | if overlap_count > 0: 312 | matches.append((Vector(i=i, j=j), int(overlap_count))) 313 | 314 | # Sort matches by overlap count if not exact matching 315 | if match_type == 'exact': return matches 316 | return sorted(matches, key=lambda x: x[1], reverse=True) 317 | 318 | @staticmethod 319 | def find_best_match( 320 | target: np.ndarray, # target array 321 | pattern: np.ndarray, # the pattern to align 322 | match_type: str = 'exact' # can be 'exact', 'allow_missing' or 'allow_extra' 323 | ) -> Tuple[Vector, int]: # tuple of (position, overlap_count) 324 | """Find the best matching position for the pattern in the target.""" 325 | matches = PatternMatcher.find_matches(target, pattern, match_type) 326 | return matches[0] if matches else (None, 0) 327 | 328 | @staticmethod 329 | def extract_matching_region( 330 | target: np.ndarray, 331 | pattern: np.ndarray, 332 | position: Vector 333 | ) -> np.ndarray: 334 | """Extract the region from the target that matches the pattern.""" 335 | h, w = pattern.shape 336 | i, j = position.i, position.j 337 | return target[i:i+h, j:j+w].copy() 338 | 339 | # %% ../nbs/01_ocm.ipynb 79 340 | class EnclosureFiller: 341 | 'Fill areas of an array that are "enclosed" by cells of a given value' 342 | @staticmethod 343 | def fill_enclosures(mask: np.ndarray) -> np.ndarray: 344 | """Fill enclosed areas in the given mask that are surrounded by boundary_value.""" 345 | # Convert mask to boolean if it is not already 346 | mask = mask.astype(bool) 347 | # Invert boundaries to mark enclosed areas as zeroes. 348 | inverted_mask = ~mask 349 | # Label connected components 350 | labeled_array, _ = ndimage.label(inverted_mask) 351 | # Create a copy of labeled_array but with zero labels at the border 352 | border_mask = np.ones_like(labeled_array) 353 | border_mask[0, :] = border_mask[-1, :] = border_mask[:, 0] = border_mask[:, -1] = 0 354 | border_labels = np.unique(labeled_array * (1 - border_mask)) 355 | # Set non-border enclosed areas to True (fill them) 356 | output_mask = np.isin(labeled_array, border_labels, invert=True) 357 | return output_mask.astype(int) 358 | 359 | # %% ../nbs/01_ocm.ipynb 80 360 | class CyclicPattern(BaseModel): 361 | """ 362 | Identify, represent, and manipulate cyclic patterns in ARC task grids, 363 | particularly for tasks involving pattern repetition and extension. 364 | """ 365 | data: np.ndarray 366 | period: int 367 | axis: int = Field(0, ge=0, le=1) 368 | 369 | model_config = {"arbitrary_types_allowed": True} 370 | 371 | @classmethod 372 | def from_array(cls, arr: np.ndarray, axis: int = 0) -> 'CyclicPattern': 373 | """Create a CyclicPattern instance from a numpy array.""" 374 | return cls(data=arr, period=cls.find_period(arr, axis), axis=axis) 375 | 376 | @staticmethod 377 | def find_period(arr: np.ndarray, axis: int = 0) -> int: 378 | """Find the smallest period along a specified axis of a NumPy array.""" 379 | n = arr.shape[axis] 380 | if n == 0: return 0 # Undefined period for empty axis 381 | 382 | for p in range(1, n): 383 | pattern = np.take(arr, indices=range(p), axis=axis) 384 | repeats = int(np.ceil(n / p)) 385 | tiled = np.concatenate([pattern] * repeats, axis=axis) 386 | slicer = [slice(None)] * arr.ndim 387 | slicer[axis] = slice(0, n) 388 | tiled = tiled[tuple(slicer)] 389 | 390 | if np.array_equal(arr, tiled): return p 391 | 392 | return n # The entire axis if no smaller period is found 393 | 394 | def extend(self, length: int) -> np.ndarray: 395 | """Extend the pattern to a specified length.""" 396 | pattern = np.take(self.data, range(self.period), axis=self.axis) 397 | repeats = [1] * self.data.ndim 398 | repeats[self.axis] = length // self.period + 1 399 | tiled = np.tile(pattern, repeats) 400 | slices = [slice(None)] * self.data.ndim 401 | slices[self.axis] = slice(length) 402 | return tiled[tuple(slices)] 403 | 404 | def to_array(self) -> np.ndarray: 405 | """Convert the cyclic pattern to a numpy array.""" 406 | return self.data 407 | 408 | def __len__(self) -> int: 409 | """Get the length of the pattern along its axis.""" 410 | return self.data.shape[self.axis] 411 | -------------------------------------------------------------------------------- /arcsolver/ocm_cleaned.py: -------------------------------------------------------------------------------- 1 | """Primitive classes for constructing object-centric models (OCMs) for ARC tasks""" 2 | 3 | __all__ = ['Vector', 'Color', 'Direction', 'Object', 'Rectangle', 'Line', 'Bitmap', 'Grid', 'ShapeExtractor', 'PatternMatcher', 4 | 'EnclosureFiller', 'CyclicPattern'] 5 | 6 | from enum import Enum 7 | from typing import ClassVar, List, Optional, Tuple, Union, Literal 8 | import numpy as np 9 | from pydantic import BaseModel, Field, field_validator, model_validator 10 | from scipy import ndimage 11 | 12 | class Vector(BaseModel): 13 | "2D vector for positions, sizes, and directions." 14 | i: int 15 | j: int 16 | 17 | def __init__(self, i: int, j: int): 18 | super().__init__(i=i, j=j) 19 | 20 | def to_array(self) -> np.ndarray: 21 | return np.array([self.i, self.j], dtype=int) 22 | 23 | @classmethod 24 | def from_array(cls, a: Union[np.ndarray, List, Tuple]) -> 'Vector': 25 | if not isinstance(a, np.ndarray): a = np.array(a) 26 | if a.shape != (2,) or a.dtype != int: 27 | raise ValueError("Need 1D array with 2 ints") 28 | return cls(i=int(a[0]), j=int(a[1])) 29 | 30 | def __add__(self, o): return Vector(i=self.i + o.i, j=self.j + o.j) 31 | def __sub__(self, o): return Vector(i=self.i - o.i, j=self.j - o.j) 32 | def __mul__(self, s): return Vector(i=self.i * s, j=self.j * s) 33 | def __rmul__(self, s): return self.__mul__(s) 34 | 35 | class Color(BaseModel): 36 | "Represents a color using an integer value (0-9) or a color name." 37 | value: int = Field(..., ge=0, le=9) 38 | colors: ClassVar[List[str]] = ['black', 'blue', 'red', 'green', 'yellow', 'grey', 'pink', 'orange', 'cyan', 'brown'] 39 | 40 | def __init__(self, value: Union[int, str]): 41 | if isinstance(value, str): value = self.colors.index(value.lower()) 42 | super().__init__(value=value) 43 | 44 | def to_array(self): return np.array([self.value], dtype=int) 45 | 46 | def __str__(self): return f"Color('{self.colors[self.value]}', value={self.value})" 47 | __repr__ = __str__ 48 | 49 | class Direction(Enum): 50 | "Represents cardinal and intercardinal directions as 2D vectors." 51 | UP = Vector(i=-1, j=0) 52 | DOWN = Vector(i=1, j=0) 53 | LEFT = Vector(i=0, j=-1) 54 | RIGHT = Vector(i=0, j=1) 55 | NE = Vector(i=-1, j=1) 56 | SW = Vector(i=1, j=-1) 57 | NW = Vector(i=-1, j=-1) 58 | SE = Vector(i=1, j=1) 59 | 60 | def to_array(self): return self.value.to_array() 61 | 62 | @classmethod 63 | def from_array(cls, arr: np.ndarray) -> 'Direction': 64 | v = Vector(*arr) 65 | for dir in cls: 66 | if dir.value == v: return dir 67 | raise ValueError(f"No direction matches {v}") 68 | 69 | class Object(BaseModel): 70 | "Base class for shape objects in ARC tasks." 71 | position: Vector 72 | 73 | def _get_shape_array(self) -> np.ndarray: 74 | "Abstract method for getting the shape's array without positioning." 75 | raise NotImplementedError("Subclasses must implement this method") 76 | 77 | def to_array(self, grid_size: Optional[Vector] = None) -> np.ndarray: 78 | """ 79 | If grid_size is provided, returns the object positioned within a grid of that size. 80 | If grid_size is None, returns just the object's array. 81 | """ 82 | obj_arr = self._get_shape_array() 83 | if not grid_size: return obj_arr 84 | 85 | full = np.full(grid_size.to_array(), -1, dtype=int) 86 | offset = self.offset if isinstance(self, Line) else Vector(i=0, j=0) 87 | obj_pos = self.position + offset 88 | 89 | if obj_pos.i >= grid_size.i or obj_pos.j >= grid_size.j: return full 90 | 91 | # Allow negative positions to crop the object 92 | if obj_pos.i < 0: 93 | obj_arr = obj_arr[-obj_pos.i:] 94 | obj_pos.i = 0 95 | if obj_pos.j < 0: 96 | obj_arr = obj_arr[:, -obj_pos.j:] 97 | obj_pos.j = 0 98 | 99 | # Crop the object if it goes beyond the grid 100 | obj_arr = obj_arr[:min(grid_size.i - obj_pos.i, obj_arr.shape[0]), 101 | :min(grid_size.j - obj_pos.j, obj_arr.shape[1])] 102 | 103 | full[obj_pos.i:obj_pos.i + obj_arr.shape[0], 104 | obj_pos.j:obj_pos.j + obj_arr.shape[1]] = obj_arr 105 | return full 106 | 107 | class Rectangle(Object): 108 | "Rectangular shape." 109 | size: Vector 110 | color: Color 111 | 112 | def _get_shape_array(self) -> np.ndarray: 113 | return np.full(self.size.to_array(), self.color.value, dtype=int) 114 | 115 | class Line(Object): 116 | "Line shape." 117 | direction: Direction 118 | length: int = Field(..., gt=0) 119 | color: Color 120 | 121 | def _get_shape_array(self) -> np.ndarray: 122 | if self.direction in [Direction.UP, Direction.DOWN]: shape = (self.length, 1) 123 | elif self.direction in [Direction.LEFT, Direction.RIGHT]: shape = (1, self.length) 124 | else: shape = (self.length, self.length) 125 | 126 | arr = np.full(shape, -1) 127 | if self.direction in [Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT]: 128 | arr.fill(self.color.value) 129 | elif self.direction in [Direction.NE, Direction.SW]: 130 | np.fill_diagonal(np.fliplr(arr), self.color.value) 131 | else: # NW, SE 132 | np.fill_diagonal(arr, self.color.value) 133 | return arr 134 | 135 | @decorator 136 | def offset(self) -> Vector: 137 | "Calculate the offset of the line based on its direction." 138 | if self.direction == Direction.UP: return Vector(-self.length + 1, 0) 139 | elif self.direction == Direction.LEFT: return Vector(0, -self.length + 1) 140 | elif self.direction == Direction.NW: return Vector(-self.length + 1, -self.length + 1) 141 | elif self.direction == Direction.NE: return Vector(-self.length + 1, 0) 142 | elif self.direction == Direction.SW: return Vector(0, -self.length + 1) 143 | return Vector(0, 0) 144 | 145 | class Bitmap(Object): 146 | "Multi-colored bitmap pattern." 147 | data: np.ndarray 148 | 149 | model_config = {"arbitrary_types_allowed": True} 150 | 151 | @field_validator('data') 152 | @classmethod 153 | def validate_data(cls, v): 154 | if not isinstance(v, np.ndarray): raise ValueError('Must be a numpy array') 155 | if v.ndim != 2: raise ValueError('Must be a 2D array') 156 | if v.dtype != int: raise ValueError('Array must contain integer values') 157 | if np.any(v < -1) or np.any(v > 9): raise ValueError('Array values must be between -1 and 9') 158 | return v 159 | 160 | def _get_shape_array(self) -> np.ndarray: return self.data 161 | 162 | @property 163 | def size(self) -> Vector: return Vector.from_array(self.data.shape) 164 | 165 | def rotate(self, n=1): return Bitmap(position=self.position, data=np.rot90(self.data, k=n)) 166 | def flip(self, axis): return Bitmap(position=self.position, data=np.flip(self.data, axis=axis)) 167 | 168 | class Grid(BaseModel): 169 | "Grid container with a size, background color, and objects." 170 | size: Vector # Size of the grid (r x c) 171 | background_color: Optional[Color] = None # Must be provided if the object layers do not cover the entire grid. 172 | objects: List[Object] = Field(default_factory=list) # List of objects contained in the grid. 173 | 174 | def to_array(self) -> np.ndarray: 175 | grid = self._create_grid(self.size, self.objects) 176 | # Only replace -1 values with background color if it exists 177 | if self.background_color is not None: 178 | grid = np.where(grid == -1, self.background_color.value, grid) 179 | return grid 180 | 181 | def _create_grid(self, size: Vector, objects: List[Object]) -> np.ndarray: 182 | "Create grid array without using background color." 183 | grid = np.full(size.to_array(), -1, dtype=int) 184 | for obj in objects: 185 | obj_array = obj.to_array(size) 186 | if self.background_color is not None: 187 | obj_array[obj_array == self.background_color.value] = -1 188 | grid = np.where(obj_array != -1, obj_array, grid) 189 | return grid 190 | 191 | @model_validator(mode='after') 192 | def validate_grid_coverage(self) -> 'Grid': 193 | if self.background_color is None: 194 | grid = self._create_grid(self.size, self.objects) 195 | if np.any(grid == -1): 196 | raise ValueError("When background_color is not provided, objects must cover the entire grid") 197 | return self 198 | 199 | class ShapeExtractor: 200 | 'Extract distinct "shapes" (i.e. contiguous regions of the same value) from a numpy array' 201 | # Define the connectivity structures as class attributes 202 | ORTH = np.array([[0,1,0], [1,1,1], [0,1,0]]) 203 | DIAG = np.ones((3,3)) 204 | 205 | @staticmethod 206 | def extract_contiguous_regions( 207 | array: np.ndarray, 208 | value: int, 209 | include_diagonal: bool = False, 210 | background_color: Optional[int] = None 211 | ) -> List[Tuple[np.ndarray, Tuple[int, int]]]: 212 | """Extract contiguous regions of a specified value from a numpy array. Can include diagonal connections if specified.""" 213 | arr = array.copy() 214 | mask = (arr == value) 215 | structure = ShapeExtractor.DIAG if include_diagonal else ShapeExtractor.ORTH 216 | labeled, _ = ndimage.label(mask, structure=structure) 217 | slices = ndimage.find_objects(labeled) 218 | 219 | regions = sorted([(np.sum(mask[s]), s, arr[s]) for s in slices], 220 | key=lambda x: x[0], reverse=True) 221 | for region in regions: 222 | if background_color is not None: region[2][region[2] == background_color] = -1 223 | 224 | return [(r[2], (r[1][0].start, r[1][1].start)) for r in regions] 225 | 226 | @staticmethod 227 | def extract_largest_shape( 228 | array: np.ndarray, 229 | value: int, 230 | include_diagonal: bool = False, 231 | background_color: Optional[int] = None 232 | ) -> Tuple[Optional[np.ndarray], Optional[Tuple[int, int]]]: 233 | """ 234 | Extract the largest contiguous shape of a specified value from a numpy array.""" 235 | regions = ShapeExtractor.extract_contiguous_regions(array, value, include_diagonal, background_color) 236 | return regions[0] if regions else (None, None) 237 | 238 | @staticmethod 239 | def extract_all_shapes( 240 | array: np.ndarray, # Numpy array 241 | include_diagonal: bool = False, # Consider diagonally adjacent cells as connected or not 242 | background_color: Optional[int] = None # Optionally specify a background color to ignore 243 | ) -> List[Tuple[np.ndarray, Tuple[int, int], int]]: # List of (sub-array, position, color_value) tuples 244 | "Extract all shapes of all values from a numpy array." 245 | all_shapes = [] 246 | for value in np.unique(array): 247 | if background_color is not None and value == background_color: continue 248 | shapes = ShapeExtractor.extract_contiguous_regions(array, value, include_diagonal, background_color) 249 | all_shapes.extend([(shape, pos, value) for shape, pos in shapes]) 250 | return all_shapes 251 | 252 | class PatternMatcher: 253 | """ 254 | A class for finding alignments between patterns in numpy arrays. 255 | Supports exact matching and partial matching with missing or extra elements. 256 | """ 257 | 258 | @staticmethod 259 | def find_matches( 260 | target: np.ndarray, # The larger array to search in 261 | pattern: np.ndarray, # The smaller array containing the pattern to match 262 | match_type: str = 'exact' # 'exact', 'allow_missing', or 'allow_extra' 263 | ) -> List[Tuple[Vector, int]]: 264 | """ 265 | Find positions where a pattern aligns with a target, sorted by overlap count 266 | for non-exact matches. 267 | """ 268 | h, w = pattern.shape 269 | H, W = target.shape 270 | matches = [] 271 | 272 | for i in range(H - h + 1): 273 | for j in range(W - w + 1): 274 | window = target[i:i+h, j:j+w] 275 | 276 | # Get masks for non-zero elements 277 | pattern_nonzero = pattern != 0 278 | window_nonzero = window != 0 279 | 280 | # Check for mismatches based on match_type 281 | if match_type != 'allow_missing': 282 | if np.any(window_nonzero & ~pattern_nonzero): continue 283 | 284 | if match_type != 'allow_extra': 285 | if np.any(pattern_nonzero & ~window_nonzero): continue 286 | 287 | # Check that overlapping non-zero elements match 288 | both_nonzero = window_nonzero & pattern_nonzero 289 | if not np.array_equal(window[both_nonzero], pattern[both_nonzero]): continue 290 | 291 | # Count overlapping non-zero elements and only include non-zero overlaps 292 | overlap_count = np.sum(both_nonzero) 293 | if overlap_count > 0: 294 | matches.append((Vector(i=i, j=j), int(overlap_count))) 295 | 296 | # Sort matches by overlap count if not exact matching 297 | if match_type == 'exact': return matches 298 | return sorted(matches, key=lambda x: x[1], reverse=True) 299 | 300 | @staticmethod 301 | def find_best_match( 302 | target: np.ndarray, # target array 303 | pattern: np.ndarray, # the pattern to align 304 | match_type: str = 'exact' # can be 'exact', 'allow_missing' or 'allow_extra' 305 | ) -> Tuple[Vector, int]: # tuple of (position, overlap_count) 306 | """Find the best matching position for the pattern in the target.""" 307 | matches = PatternMatcher.find_matches(target, pattern, match_type) 308 | return matches[0] if matches else (None, 0) 309 | 310 | @staticmethod 311 | def extract_matching_region( 312 | target: np.ndarray, 313 | pattern: np.ndarray, 314 | position: Vector 315 | ) -> np.ndarray: 316 | """Extract the region from the target that matches the pattern.""" 317 | h, w = pattern.shape 318 | i, j = position.i, position.j 319 | return target[i:i+h, j:j+w].copy() 320 | 321 | class EnclosureFiller: 322 | 'Fill areas of an array that are "enclosed" by cells of a given value' 323 | @staticmethod 324 | def fill_enclosures(mask: np.ndarray) -> np.ndarray: 325 | """Fill enclosed areas in the given mask that are surrounded by boundary_value.""" 326 | # Convert mask to boolean if it is not already 327 | mask = mask.astype(bool) 328 | # Invert boundaries to mark enclosed areas as zeroes. 329 | inverted_mask = ~mask 330 | # Label connected components 331 | labeled_array, _ = ndimage.label(inverted_mask) 332 | # Create a copy of labeled_array but with zero labels at the border 333 | border_mask = np.ones_like(labeled_array) 334 | border_mask[0, :] = border_mask[-1, :] = border_mask[:, 0] = border_mask[:, -1] = 0 335 | border_labels = np.unique(labeled_array * (1 - border_mask)) 336 | # Set non-border enclosed areas to True (fill them) 337 | output_mask = np.isin(labeled_array, border_labels, invert=True) 338 | return output_mask.astype(int) 339 | 340 | class CyclicPattern(BaseModel): 341 | """ 342 | Identify, represent, and manipulate cyclic patterns in ARC task grids, 343 | particularly for tasks involving pattern repetition and extension. 344 | """ 345 | data: np.ndarray 346 | period: int 347 | axis: int = Field(0, ge=0, le=1) 348 | 349 | model_config = {"arbitrary_types_allowed": True} 350 | 351 | @classmethod 352 | def from_array(cls, arr: np.ndarray, axis: int = 0) -> 'CyclicPattern': 353 | """Create a CyclicPattern instance from a numpy array.""" 354 | return cls(data=arr, period=cls.find_period(arr, axis), axis=axis) 355 | 356 | @staticmethod 357 | def find_period(arr: np.ndarray, axis: int = 0) -> int: 358 | """Find the smallest period along a specified axis of a NumPy array.""" 359 | n = arr.shape[axis] 360 | if n == 0: return 0 # Undefined period for empty axis 361 | 362 | for p in range(1, n): 363 | pattern = np.take(arr, indices=range(p), axis=axis) 364 | repeats = int(np.ceil(n / p)) 365 | tiled = np.concatenate([pattern] * repeats, axis=axis) 366 | slicer = [slice(None)] * arr.ndim 367 | slicer[axis] = slice(0, n) 368 | tiled = tiled[tuple(slicer)] 369 | 370 | if np.array_equal(arr, tiled): return p 371 | 372 | return n # The entire axis if no smaller period is found 373 | 374 | def extend(self, length: int) -> np.ndarray: 375 | """Extend the pattern to a specified length.""" 376 | pattern = np.take(self.data, range(self.period), axis=self.axis) 377 | repeats = [1] * self.data.ndim 378 | repeats[self.axis] = length // self.period + 1 379 | tiled = np.tile(pattern, repeats) 380 | slices = [slice(None)] * self.data.ndim 381 | slices[self.axis] = slice(length) 382 | return tiled[tuple(slices)] 383 | 384 | def to_array(self) -> np.ndarray: 385 | """Convert the cyclic pattern to a numpy array.""" 386 | return self.data 387 | 388 | def __len__(self) -> int: 389 | """Get the length of the pattern along its axis.""" 390 | return self.data.shape[self.axis] -------------------------------------------------------------------------------- /arcsolver/score.py: -------------------------------------------------------------------------------- 1 | """Functions for scoring predicted ARC grids against ground truth output grids""" 2 | 3 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_score.ipynb. 4 | 5 | # %% auto 0 6 | __all__ = ['score'] 7 | 8 | # %% ../nbs/06_score.ipynb 3 9 | import numpy as np 10 | from .task import ArcGrid, ArcPair 11 | 12 | # %% ../nbs/06_score.ipynb 10 13 | def score( 14 | truth: ArcGrid, # True ARC grid 15 | pred: ArcGrid | None # Predicted ARC grid 16 | ) -> float: 17 | "Score a predicted grid against the true grid" 18 | if pred is None: return 0.0 19 | if pred == truth: return 1.0 20 | 21 | # Calculate shape penalty 22 | rows_ratio = min(truth.shape[0], pred.shape[0]) / max(truth.shape[0], pred.shape[0]) 23 | cols_ratio = min(truth.shape[1], pred.shape[1]) / max(truth.shape[1], pred.shape[1]) 24 | shape_penalty = rows_ratio * cols_ratio 25 | 26 | # Get overlapping region dimensions 27 | overlap_rows = min(truth.shape[0], pred.shape[0]) 28 | overlap_cols = min(truth.shape[1], pred.shape[1]) 29 | 30 | # Calculate color accuracy in overlapping region 31 | true_overlap = truth.data[:overlap_rows, :overlap_cols] 32 | pred_overlap = pred.data[:overlap_rows, :overlap_cols] 33 | color_accuracy = np.mean(true_overlap == pred_overlap) 34 | 35 | return float(shape_penalty * color_accuracy) 36 | -------------------------------------------------------------------------------- /arcsolver/task.py: -------------------------------------------------------------------------------- 1 | """Classes for representing and visualizing ARC tasks""" 2 | 3 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_task.ipynb. 4 | 5 | # %% auto 0 6 | __all__ = ['train_tasks', 'eval_tasks', 'ArcGrid', 'ArcPair', 'ArcTask', 'get_task_files'] 7 | 8 | # %% ../nbs/00_task.ipynb 3 9 | import os 10 | import json 11 | import numpy as np 12 | from pathlib import Path 13 | from typing import Union, Optional, Literal 14 | from fastcore.utils import * 15 | import matplotlib.pyplot as plt 16 | from matplotlib.transforms import Bbox 17 | from matplotlib.gridspec import GridSpec, GridSpecFromSubplotSpec 18 | from io import BytesIO 19 | import importlib.resources as resources 20 | 21 | # %% ../nbs/00_task.ipynb 4 22 | class ArcGrid: 23 | "A single ARC grid" 24 | # Class-level color mapping 25 | _colors = ['black', 'blue', 'red', 'green', 'yellow', 'grey', 'pink', 'orange', 'cyan', 'brown'] 26 | _color_mapping = {i: color for i, color in enumerate(_colors)} 27 | 28 | def __init__(self, 29 | data: np.ndarray # 2d array of integers (0–9) 30 | ): 31 | if not isinstance(data, np.ndarray): 32 | data = np.array(data) 33 | if data.ndim != 2: 34 | raise ValueError("Data must be a 2D array") 35 | if not np.issubdtype(data.dtype, np.integer): 36 | raise ValueError("Data must contain integers") 37 | if not ((data >= 0) & (data <= 9)).all(): 38 | raise ValueError("Data values must be between 0 and 9") 39 | if data.shape[0] > 30 or data.shape[1] > 30: 40 | raise ValueError("Data dimensions cannot exceed 30") 41 | 42 | self.data = data 43 | self.shape = data.shape 44 | 45 | def __str__(self): return f"Grid(shape={self.data.shape})" 46 | __repr__ = __str__ 47 | 48 | def plot(self, 49 | ax: plt.Axes|None = None, # matplotlib `Axes` object to plot on 50 | title: str|None = None, # title for the plot 51 | max_width: int|None = None, # maximum width for consistent sizing across multiple grids 52 | max_height: int|None = None, # maximum height for consistent sizing across multiple grids 53 | to_base64: bool = False, # If True, returns plot image as base64 string 54 | **kwargs # 55 | ): 56 | "Plot a single ARC grid" 57 | created_fig = ax is None 58 | if created_fig: fig, ax = plt.subplots(figsize=(4, 4)) 59 | else: fig = ax.figure 60 | 61 | max_width = self.shape[1] if max_width is None else max_width 62 | max_height = self.shape[0] if max_height is None else max_height 63 | 64 | self._setup_ax(ax, max_width, max_height, title) 65 | self._draw_grid(ax, max_width, max_height) 66 | 67 | if created_fig and to_base64: 68 | buffer = BytesIO() 69 | fig.savefig(buffer, format='png', bbox_inches='tight', 70 | facecolor='white', transparent=False, **kwargs) 71 | plt.close(fig) 72 | buffer.seek(0) 73 | image_bytes = buffer.getvalue() 74 | buffer.close() 75 | return image_bytes # base64.b64encode(image_bytes).decode() 76 | elif created_fig: 77 | plt.show() 78 | return None 79 | return ax 80 | 81 | def _setup_ax(self, ax: plt.Axes, max_width: int, max_height: int, title: Optional[str] = None) -> None: 82 | """Set up a matplotlib axis for grid plotting""" 83 | ax.set_xlim(0, max_width) 84 | ax.set_ylim(max_height, 0) 85 | ax.set_aspect('equal') 86 | if title: 87 | ax.set_title(title) 88 | 89 | ax.set_xticks([]) 90 | ax.set_yticks([]) 91 | ax.patch.set_alpha(0.0) 92 | 93 | for spine in ax.spines.values(): 94 | spine.set_visible(False) 95 | 96 | def _draw_grid(self, ax: plt.Axes, max_width: int, max_height: int) -> None: 97 | """Draw the grid cells and lines""" 98 | # Draw cells 99 | for row in range(self.shape[0]): 100 | for col in range(self.shape[1]): 101 | color_index = self.data[row, col] 102 | cell_color = self._color_mapping[color_index] 103 | ax.add_patch(plt.Rectangle((col, row), 1, 1, 104 | facecolor=cell_color, 105 | edgecolor='none')) 106 | 107 | # Draw grid lines 108 | for x in range(self.shape[1] + 1): 109 | ax.axvline(x, color='w', linewidth=1.5, 110 | ymin=(max_height-self.shape[0])/max_height, ymax=1) 111 | for y in range(self.shape[0] + 1): 112 | ax.axhline(y, color='w', linewidth=1.5, 113 | xmin=0, xmax=self.shape[1]/max_width) 114 | 115 | # %% ../nbs/00_task.ipynb 12 116 | @patch 117 | def __eq__(self: ArcGrid, other: ArcGrid) -> bool: 118 | return np.array_equal(self.data, other.data) 119 | 120 | # %% ../nbs/00_task.ipynb 14 121 | class ArcPair: 122 | "A pair of ARC grids, typically [input, output]. Can also be used for [output, prediction]" 123 | def __init__(self, 124 | input_grid: ArcGrid|np.ndarray, # Input grid 125 | output_grid: ArcGrid|np.ndarray, # Output grid 126 | ): 127 | self.input = input_grid if isinstance(input_grid, ArcGrid) else ArcGrid(input_grid) 128 | self.output = output_grid if isinstance(output_grid, ArcGrid) else ArcGrid(output_grid) 129 | 130 | def __str__(self): 131 | return f"ArcPair(input_shape={self.input.data.shape}, output_shape={self.output.data.shape})" 132 | __repr__ = __str__ 133 | 134 | def __iter__(self): 135 | yield self.input 136 | yield self.output 137 | 138 | def __getitem__(self, key: Union[int, str]) -> ArcGrid: 139 | if isinstance(key, int): 140 | if key == 0: return self.input 141 | elif key == 1: return self.output 142 | else: raise IndexError("Index out of range. Only indices 0 and 1 are valid.") 143 | elif isinstance(key, str): 144 | if key == 'input': return self.input 145 | elif key == 'output': return self.output 146 | else: raise KeyError("Key must be either 'input' or 'output'.") 147 | else: 148 | raise TypeError("Key must be an integer (0 or 1) or string ('input' or 'output')") 149 | 150 | def __len__(self): 151 | return 2 152 | 153 | def plot(self, 154 | titles: List[str] = ['Input', 'Output'], 155 | same_scale: bool = True, 156 | fig: Optional[plt.Figure] = None, 157 | subplot_spec: Optional[GridSpec] = None, 158 | to_base64: bool = False, 159 | **kwargs) -> Union[None, Tuple[plt.Figure, List[plt.Axes]], str]: 160 | """Plot the input-output pair side by side""" 161 | if same_scale: 162 | max_width = max(self.input.shape[1], self.output.shape[1]) 163 | max_height = max(self.input.shape[0], self.output.shape[0]) 164 | else: 165 | max_width = max_height = None 166 | 167 | # Create figure or get subplot region 168 | if fig is None: 169 | fig = plt.figure(figsize=(8, 4)) # Fixed size for pairs 170 | # fig.patch.set_facecolor('#F0F0F0') 171 | fig.patch.set_alpha(0.0) 172 | gs = GridSpec(1, 2, figure=fig, wspace=0.3) 173 | else: 174 | gs = GridSpecFromSubplotSpec(1, 2, subplot_spec=subplot_spec, wspace=0.3) 175 | 176 | # Create axes and plot grids 177 | axes = [] 178 | for i, (grid, title) in enumerate(zip([self.input, self.output], titles)): 179 | ax = fig.add_subplot(gs[0, i]) 180 | grid.plot(ax=ax, title=title, max_width=max_width, max_height=max_height) 181 | axes.append(ax) 182 | 183 | # Add separator if this is a standalone plot 184 | if subplot_spec is None: 185 | self._create_separator(fig, axes[0].get_position(), axes[1].get_position()) 186 | if to_base64: 187 | buffer = BytesIO() 188 | fig.savefig(buffer, format='png', bbox_inches='tight', 189 | # facecolor='#F0F0F0', transparent=False, 190 | **kwargs) 191 | plt.close(fig) 192 | buffer.seek(0) 193 | image_bytes = buffer.getvalue() 194 | buffer.close() 195 | return image_bytes #base64.b64encode(image_bytes).decode() 196 | else: 197 | plt.show() 198 | return None 199 | 200 | return fig, axes 201 | 202 | @staticmethod 203 | def _create_separator(fig: plt.Figure, bbox1: Bbox, bbox2: Bbox) -> None: 204 | """Create a separator line between two subplots""" 205 | middle_x = (bbox1.x1 + bbox2.x0) / 2 206 | line = plt.Line2D([middle_x, middle_x], [0.2, 0.8], 207 | transform=fig.transFigure, 208 | color='black', 209 | linewidth=1) 210 | fig.add_artist(line) 211 | 212 | # %% ../nbs/00_task.ipynb 18 213 | class ArcTask: 214 | "An ARC task" 215 | def __init__(self, 216 | task_id: str, # 8-digit task id 217 | split: str = 'train', # ARC public dataset split ('train' or 'eval') 218 | data_dir: str|Path|None = None # Path to ARC data directory (defaults to `'/path/to/arcsolver/arc_data/data'`) 219 | ): 220 | if split not in ['train', 'eval']: 221 | raise ValueError("`split` must be either 'train' or 'eval'") 222 | if data_dir is None: 223 | data_split = 'training' if split == 'train' else 'evaluation' 224 | pkg_files = resources.files("arcsolver") 225 | data_dir = pkg_files / "arc_data" / "data" / data_split 226 | 227 | self.task_id, self.split, self.data_dir = task_id, split, Path(data_dir) 228 | self.train, self.test = self._load_data() 229 | 230 | def _load_data(self): 231 | with open(self.data_dir / f"{self.task_id}.json", 'r') as f: 232 | task_data = json.load(f) 233 | 234 | return ([ArcPair(np.array(example['input']), np.array(example['output'])) 235 | for example in task_data[split]] 236 | for split in ('train', 'test')) 237 | 238 | def __str__(self) -> str: 239 | return (f"ArcTask(id='{self.task_id}', " 240 | f"train_examples={len(self.train)}, " 241 | f"test_examples={len(self.test)})") 242 | __repr__ = __str__ 243 | 244 | def plot(self, 245 | same_scale: bool = True, 246 | to_base64: bool = False, 247 | **kwargs) -> Union[None, str]: 248 | """Plot all training examples in the task""" 249 | n_examples = len(self.train) 250 | 251 | if n_examples == 0: 252 | print("No training examples available") 253 | return None 254 | 255 | # Fixed width, height scales with number of examples 256 | fig_width = 8 # Fixed width 257 | fig_height = 4 * n_examples # Each example gets fixed height 258 | 259 | # Create figure and grid of subplots 260 | fig = plt.figure(figsize=(fig_width, fig_height)) 261 | # fig.suptitle(f'Task {self.task_id} - Training Examples', y=0.98) 262 | # fig.patch.set_facecolor('#F0F0F0') 263 | fig.patch.set_alpha(0.0) 264 | 265 | # Create vertical stack of subplots 266 | gs = GridSpec(n_examples, 1, figure=fig, hspace=0.15) 267 | 268 | # Plot each training example 269 | for i, pair in enumerate(self.train): 270 | titles = [f'Example {i+1} Input', f'Example {i+1} Output'] 271 | pair.plot(titles=titles, same_scale=same_scale, 272 | fig=fig, subplot_spec=gs[i]) 273 | 274 | if to_base64: 275 | buffer = BytesIO() 276 | fig.savefig(buffer, format='png', bbox_inches='tight', 277 | # facecolor='#F0F0F0', transparent=False, 278 | **kwargs) 279 | plt.close(fig) 280 | buffer.seek(0) 281 | image_bytes = buffer.getvalue() 282 | buffer.close() 283 | return image_bytes # base64.b64encode(image_bytes).decode() 284 | else: 285 | plt.show() 286 | return None 287 | 288 | # %% ../nbs/00_task.ipynb 25 289 | def get_task_files(split: str # 'train' or 'eval' 290 | ) -> list[str]: 291 | "Get list of files from either training or evaluation data." 292 | data_split = 'training' if split == 'train' else 'evaluation' 293 | pkg_files = resources.files("arcsolver") 294 | data_path = pkg_files / "arc_data" / "data" / data_split 295 | return [f.split('.json')[0] for f in os.listdir(data_path)] 296 | 297 | # %% ../nbs/00_task.ipynb 26 298 | train_tasks = get_task_files('train') 299 | eval_tasks = get_task_files('eval') 300 | -------------------------------------------------------------------------------- /arcsolver/utils.py: -------------------------------------------------------------------------------- 1 | """an xml parsing util""" 2 | 3 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_utils.ipynb. 4 | 5 | # %% auto 0 6 | __all__ = ['parse_from_xml'] 7 | 8 | # %% ../nbs/04_utils.ipynb 3 9 | import re 10 | 11 | # %% ../nbs/04_utils.ipynb 4 12 | class TagNotFoundError(Exception): 13 | """Raised when the specified tag is not found in the XML.""" 14 | pass 15 | 16 | 17 | class NoContentError(Exception): 18 | """Raised when the specified tag is found but contains no content.""" 19 | pass 20 | 21 | 22 | class MultipleTagsError(Exception): 23 | """ 24 | Raised when multiple instances of the specified tag are found in the XML. 25 | """ 26 | pass 27 | 28 | # %% ../nbs/04_utils.ipynb 5 29 | def parse_from_xml(input_text: str, tag_name: str) -> str: 30 | """ 31 | Parse content from an XML-like string for a specific tag. 32 | 33 | Args: 34 | input_text (str): The input text containing XML-like tags. 35 | tag_name (str): The name of the tag to parse. 36 | 37 | Returns: 38 | str: The content within the specified tag. 39 | 40 | Raises: 41 | TagNotFoundError: If the specified tag is not found in the text. 42 | MultipleTagsError: If multiple instances of the tag are found. 43 | NoContentError: If the tag is present but contains no content. 44 | """ 45 | pattern = rf"<{tag_name}>(.*?)" 46 | matches = re.findall(pattern, input_text, re.DOTALL) 47 | 48 | if not matches: 49 | raise TagNotFoundError(f"Tag '{tag_name}' not found in the text.") 50 | 51 | if len(matches) > 1: 52 | raise MultipleTagsError( 53 | f"Multiple instances of tag '{tag_name}' found in the text." 54 | ) 55 | 56 | content = matches[0].strip() 57 | content = content.replace("```python", "").replace("```", "").strip() 58 | 59 | if not content: 60 | raise NoContentError( 61 | f"Tag '{tag_name}' is present but contains no content." 62 | ) 63 | 64 | return content 65 | 66 | -------------------------------------------------------------------------------- /index_files/figure-commonmark/cell-2-output-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agemoai/arcsolver/fe9e1c43fde5d68e4f0ee4e74dc0939283c11e46/index_files/figure-commonmark/cell-2-output-1.png -------------------------------------------------------------------------------- /index_files/figure-commonmark/cell-4-output-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agemoai/arcsolver/fe9e1c43fde5d68e4f0ee4e74dc0939283c11e46/index_files/figure-commonmark/cell-4-output-1.png -------------------------------------------------------------------------------- /index_files/figure-html/cell-2-output-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agemoai/arcsolver/fe9e1c43fde5d68e4f0ee4e74dc0939283c11e46/index_files/figure-html/cell-2-output-1.png -------------------------------------------------------------------------------- /index_files/figure-html/cell-4-output-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agemoai/arcsolver/fe9e1c43fde5d68e4f0ee4e74dc0939283c11e46/index_files/figure-html/cell-4-output-1.png -------------------------------------------------------------------------------- /nbs/06_score.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "a4b36c30-f756-426c-9c40-582a5612699e", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "#| default_exp score" 11 | ] 12 | }, 13 | { 14 | "attachments": {}, 15 | "cell_type": "markdown", 16 | "id": "aeca1ec4-71ee-4aad-80a8-741aa134257c", 17 | "metadata": {}, 18 | "source": [ 19 | "# score\n", 20 | "\n", 21 | "> Functions for scoring predicted ARC grids against ground truth output grids" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "018f0fe4-a2dd-4c96-a271-023aacc72ba9", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "#| hide\n", 32 | "from nbdev.showdoc import *" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "d887a34b-dc31-4220-a695-dc0efbb2dfa3", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "#| export\n", 43 | "import numpy as np\n", 44 | "from arcsolver.task import ArcGrid, ArcPair" 45 | ] 46 | }, 47 | { 48 | "attachments": {}, 49 | "cell_type": "markdown", 50 | "id": "2844ff2b-aa33-4490-87d0-10cddac2d03f", 51 | "metadata": {}, 52 | "source": [ 53 | "We need a way of evaluating predicted ARC grids against the true output grids.\n", 54 | "\n", 55 | "A simple approach is to calculate the proportion of correct cells." 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "id": "8440d572-1b8c-42c5-a2ae-f0a6cb7bbce7", 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "def score(\n", 66 | " truth: ArcGrid, # True ARC grid\n", 67 | " pred: ArcGrid # Predicted ARC grid\n", 68 | ") -> float:\n", 69 | " \"Score a predicted grid against the true grid\"\n", 70 | " if pred == truth: return 1.0\n", 71 | "\n", 72 | " return float(np.mean(pred.data == truth.data))" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "id": "31ef5892-759d-4700-a77e-adcfe0bc938b", 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "pair = ArcPair(input_grid = np.array([[1,2,3],[4,5,6]]),\n", 83 | " output_grid = np.array([[3,2,1],[4,5,6]]))" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "id": "0c7caa95-eaee-4a71-ae28-00f4c68e939e", 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "data": { 94 | "image/png": "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", 95 | "text/plain": [ 96 | "
" 97 | ] 98 | }, 99 | "metadata": {}, 100 | "output_type": "display_data" 101 | } 102 | ], 103 | "source": [ 104 | "pair.plot(titles=['Truth', 'Prediction'])" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "id": "edc0ec41-c469-4b18-bdaf-e2c77388db49", 111 | "metadata": {}, 112 | "outputs": [ 113 | { 114 | "data": { 115 | "text/plain": [ 116 | "0.6666666666666666" 117 | ] 118 | }, 119 | "execution_count": null, 120 | "metadata": {}, 121 | "output_type": "execute_result" 122 | } 123 | ], 124 | "source": [ 125 | "score(*pair)" 126 | ] 127 | }, 128 | { 129 | "attachments": {}, 130 | "cell_type": "markdown", 131 | "id": "de8a6861-f4d4-4a24-878f-63fcbc6e68bd", 132 | "metadata": {}, 133 | "source": [ 134 | "\n", 135 | "Often the predicted grid has the wrong shape. We could simply return 0.0. Instead, let's pad the grids to be equal shape and assign partial credit to correctly predicted cells in the overlapping region" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "id": "956d0a44-5ea1-4923-a454-ad3211109a5a", 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "#| exports\n", 146 | "def score(\n", 147 | " truth: ArcGrid, # True ARC grid\n", 148 | " pred: ArcGrid | None # Predicted ARC grid\n", 149 | ") -> float:\n", 150 | " \"Score a predicted grid against the true grid\"\n", 151 | " if pred is None: return 0.0\n", 152 | " if pred == truth: return 1.0\n", 153 | " \n", 154 | " # Calculate shape penalty\n", 155 | " rows_ratio = min(truth.shape[0], pred.shape[0]) / max(truth.shape[0], pred.shape[0])\n", 156 | " cols_ratio = min(truth.shape[1], pred.shape[1]) / max(truth.shape[1], pred.shape[1])\n", 157 | " shape_penalty = rows_ratio * cols_ratio\n", 158 | "\n", 159 | " # Get overlapping region dimensions\n", 160 | " overlap_rows = min(truth.shape[0], pred.shape[0])\n", 161 | " overlap_cols = min(truth.shape[1], pred.shape[1])\n", 162 | "\n", 163 | " # Calculate color accuracy in overlapping region\n", 164 | " true_overlap = truth.data[:overlap_rows, :overlap_cols]\n", 165 | " pred_overlap = pred.data[:overlap_rows, :overlap_cols]\n", 166 | " color_accuracy = np.mean(true_overlap == pred_overlap)\n", 167 | "\n", 168 | " return float(shape_penalty * color_accuracy)" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": null, 174 | "id": "b5cfbe75-ab95-4193-9363-58658ed47d9c", 175 | "metadata": {}, 176 | "outputs": [ 177 | { 178 | "data": { 179 | "text/plain": [ 180 | "0.6666666666666666" 181 | ] 182 | }, 183 | "execution_count": null, 184 | "metadata": {}, 185 | "output_type": "execute_result" 186 | } 187 | ], 188 | "source": [ 189 | "score(*pair)" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "id": "190129ec-06c7-4b88-8fd9-19f75f2288e7", 196 | "metadata": {}, 197 | "outputs": [ 198 | { 199 | "data": { 200 | "image/png": "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", 201 | "text/plain": [ 202 | "
" 203 | ] 204 | }, 205 | "metadata": {}, 206 | "output_type": "display_data" 207 | } 208 | ], 209 | "source": [ 210 | "pair = ArcPair(input_grid = np.array([[1,2,3],[4,5,6]]),\n", 211 | " output_grid = np.array([[3,2,1],[4,5,6], [7,8,9]]))\n", 212 | "pair.plot(titles=['Truth', 'Prediction'])" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "id": "67da378b-83f9-4610-9b88-d5abd12f1bb3", 219 | "metadata": {}, 220 | "outputs": [ 221 | { 222 | "data": { 223 | "text/plain": [ 224 | "0.4444444444444444" 225 | ] 226 | }, 227 | "execution_count": null, 228 | "metadata": {}, 229 | "output_type": "execute_result" 230 | } 231 | ], 232 | "source": [ 233 | "score(*pair)" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "id": "5c1aca35-251c-44db-a5f8-2e02a261c9ad", 240 | "metadata": {}, 241 | "outputs": [ 242 | { 243 | "data": { 244 | "image/png": "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", 245 | "text/plain": [ 246 | "
" 247 | ] 248 | }, 249 | "metadata": {}, 250 | "output_type": "display_data" 251 | } 252 | ], 253 | "source": [ 254 | "pair = ArcPair(input_grid = np.array([[1,2,3],[4,5,6],[7,8,9]]),\n", 255 | " output_grid = np.array([[3,2,1],[4,5,6]]))\n", 256 | "pair.plot(titles=['Truth', 'Prediction'])" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": null, 262 | "id": "bd2e008e-65cb-4529-ae0c-97602ec5513b", 263 | "metadata": {}, 264 | "outputs": [ 265 | { 266 | "data": { 267 | "text/plain": [ 268 | "0.4444444444444444" 269 | ] 270 | }, 271 | "execution_count": null, 272 | "metadata": {}, 273 | "output_type": "execute_result" 274 | } 275 | ], 276 | "source": [ 277 | "score(*pair)" 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": null, 283 | "id": "677b780e-70ee-44d6-9cdd-0499d7e61d0f", 284 | "metadata": {}, 285 | "outputs": [], 286 | "source": [ 287 | "#| hide\n", 288 | "import nbdev; nbdev.nbdev_export()" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": null, 294 | "id": "2a3034e4-c979-4788-b359-76a41fb523e0", 295 | "metadata": {}, 296 | "outputs": [], 297 | "source": [] 298 | } 299 | ], 300 | "metadata": { 301 | "kernelspec": { 302 | "display_name": "python3", 303 | "language": "python", 304 | "name": "python3" 305 | } 306 | }, 307 | "nbformat": 4, 308 | "nbformat_minor": 5 309 | } 310 | -------------------------------------------------------------------------------- /nbs/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | 4 | format: 5 | html: 6 | theme: cosmo 7 | css: styles.css 8 | toc: true 9 | keep-md: true 10 | commonmark: default 11 | 12 | website: 13 | twitter-card: true 14 | open-graph: true 15 | repo-actions: [issue] 16 | navbar: 17 | background: primary 18 | search: true 19 | sidebar: 20 | style: floating 21 | 22 | metadata-files: [nbdev.yml, sidebar.yml] -------------------------------------------------------------------------------- /nbs/index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# arcsolver\n", 8 | "\n", 9 | "> A Python library for automatically solving ARC challenges using Claude and object-centric modeling." 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "This library contains tools for visualizing, analyzing and solving tasks from the Abstraction and Reasoning Corpus (ARC) challenge dataset.\n", 17 | "\n", 18 | "As this library was built using [`nbdev`](https://github.com/AnswerDotAI/claudette.git), the source code can be found in the jupyter notebooks directory ([nbs](https://github.com/agemoai/arcsolver/tree/main/nbs)).\n", 19 | "\n", 20 | "Full documentation available at https://agemoai.github.io/arcsolver." 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "## Installation" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "1. Install `claudette` from its GitHub [repository][repo] (PyPi version is a bit behind):\n", 35 | "\n", 36 | "```sh\n", 37 | "$ pip install git+https://github.com/AnswerDotAI/claudette.git@5ea3a59\n", 38 | "```\n", 39 | "\n", 40 | "2. Install `arcsolver`:\n", 41 | "\n", 42 | "\n", 43 | "```sh\n", 44 | "$ pip install arcsolver\n", 45 | "```\n", 46 | "\n", 47 | "[repo]: https://github.com/AnswerDotAI/claudette" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "::: {.callout-note}\n", 55 | "To use the automated description or solution generation features of this library, access to Anthropic's Claude Sonnet 3.5 model is required. Set the `ANTHROPIC_API_KEY` environment variable or configure appropriate credentials for AWS bedrock or Google Vertex.\n", 56 | ":::" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "## Key Features" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "- **Task Management:** Load and visualize ARC tasks with the `ArcTask` class\n", 71 | "- **Object-Centric Modelling:** A set of primitive classes for representing grid objects and transformations\n", 72 | "- **LLM Integration:** Designed to use Claude Sonnet 3.5 for automated task analysis and solution generation\n", 73 | "- **Extensible Architecture:** Easy to add new primitives and helper functions to enhance solving capabilities" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "## Quick Start" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "### Task Representation\n", 88 | "\n", 89 | "The `task` module provides classes for working with ARC tasks" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": {}, 96 | "outputs": [ 97 | { 98 | "data": { 99 | "image/png": "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ARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoJlL1mm8WCwOq44jN+/L4tIT1K9ZX07s66VfF7yT1JfjdpLGssP7Xb8ufB36tR81DMN+2+67IcCkjruADWfeBda1r3nXIWAAgGbWOgS8tbWV5XJ5WLUcqcVikdOnTydJtp61leVnT0i/Ll3k9FOmfp3U12srOSHdymKRTN06wR3jIE7s59i8e8HbOe/q14Vu3q/9WCsALpfLEzNQc8vPLk/MRDR3Yl+v5cnJSTuc2I5xECf2c2ze3Sj6dfI4BAwA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0IwACADQjAAIANCMAAgA0Mwl6zReLBaHVceRm/dlcekJ6tesLyf29To53drZlxPbMQ7ixH6OzbsXvJ3zrn5d6NbtSw3DsN+2+24IMKnjLmDDmXeBde1r3nUIGACgmbUOAef3rktuueVwKjlqF12UPOC+SZKtra0sl8vjrec8WSwWOX36dBL92gQd+sXBnNT3xdaztrL87Anp16WLnH7Kyf4cb20lJ6RbWSyS7enppL5e+7FeALzllpMTAGeWy+WJeQPM6ddmOan94mBO6vti+dnliQmAcyf29VqenAA4d1Jfr/1wCBgAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoBkBEACgGQEQAKAZARAAoJlL1mp90QnKi7O+LBaLYyzk/Jr3Rb8ufB36xcGcpLHc8X6/9AT169KT/zk+Qd3a0ZeT+nrtRw3DsN+2+24IMKnjLmDDmXeBde1r3j1Bu/QAANiPtQ4Bb21tZblcHlYtR2qxWOT06dNJkq1nbWX52RPSr0sXOf2UqV8n9fXSrwvevF8c0O9dl9xyy3FXcX5cdFHygPsmMe9ugp3zU3JCupXFIvn89HRiO3Z2awXA5XJ5Yt7Yc8vPLk/MRDR3Yl8v/aKTW245OQFwxry7WZbLk5OTdjixHTs7h4ABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJoRAAEAmhEAAQCaEQABAJq5ZJ3Gi8XisOo4cvO+LC49Qf2a9eXEvl76dcE7SX05dhedoO/ps76Ydy98O+enYyzkPNvRlxPbsbOrYRj223bfDQEmddwFbDjzLrCufc276+wBNJEDHC3zLnAoTtCxBQAA9kMABABoRgAEAGhGAAQAaEYABABoRgAEAGhGAAQAaEYABABoRgAEAGjm/weBW8HWBHJoJAAAAABJRU5ErkJggg==", 100 | "text/plain": [ 101 | "
" 102 | ] 103 | }, 104 | "metadata": {}, 105 | "output_type": "display_data" 106 | } 107 | ], 108 | "source": [ 109 | "from arcsolver.task import ArcGrid, ArcPair, ArcTask\n", 110 | "\n", 111 | "task = ArcTask('1e0a9b12'); task.plot()" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "An `ArcTask` comprises a list of input-output example `ArcPair`s, each of which holds two `ArcGrid`s. Each class has convenient `plot` methods for visualization or directly outputting to binary strings that can be passed to Claude." 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [ 126 | { 127 | "name": "stdout", 128 | "output_type": "stream", 129 | "text": [ 130 | "Input grid 1 plot: b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01H'...\n" 131 | ] 132 | } 133 | ], 134 | "source": [ 135 | "print(f\"Input grid 1 plot: {task.train[0].input.plot(to_base64=True)[:20]}...\")" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "### Object-centric Models\n", 143 | "\n", 144 | "The `ocm` module provides a set of primitive classes for constructing object-centric models of ARC grids. For example:" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": {}, 151 | "outputs": [ 152 | { 153 | "data": { 154 | "image/png": "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", 155 | "text/plain": [ 156 | "
" 157 | ] 158 | }, 159 | "metadata": {}, 160 | "output_type": "display_data" 161 | } 162 | ], 163 | "source": [ 164 | "from arcsolver.ocm import Vector, Rectangle, Line, Grid, Color, Direction\n", 165 | "\n", 166 | "grid = Grid(\n", 167 | " size=Vector(8,8),\n", 168 | " background_color=Color('grey'),\n", 169 | " objects=[\n", 170 | " Rectangle(position=Vector(1,1), size=Vector(2,2), color=Color('red')),\n", 171 | " Line(position=Vector(6,1), direction=Direction.NE, length=6, color=Color('pink'))\n", 172 | " ]\n", 173 | ")\n", 174 | "ArcGrid(grid.to_array()).plot()" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "### Task Descriptions\n", 182 | "\n", 183 | "Use Claude to analyze and describe ARC tasks" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": null, 189 | "metadata": {}, 190 | "outputs": [ 191 | { 192 | "name": "stdout", 193 | "output_type": "stream", 194 | "text": [ 195 | "The input grids contain various colored squares arranged on a black background in different positions. In the transformation, all colored squares \"fall\" vertically to the bottom row while maintaining their relative horizontal positions and original colors. The rest of the grid becomes filled with black squares, resulting in an output where all non-black squares are aligned in the bottom row, preserving their left-to-right ordering from the input grid.\n" 196 | ] 197 | } 198 | ], 199 | "source": [ 200 | "#| output: false\n", 201 | "#| eval: false\n", 202 | "from arcsolver.describe import DescriptionGenerator\n", 203 | "\n", 204 | "describer = DescriptionGenerator()\n", 205 | "d = await describer.describe_task(task, 1); print(d[0].d)" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "metadata": {}, 212 | "outputs": [ 213 | { 214 | "name": "stdout", 215 | "output_type": "stream", 216 | "text": [ 217 | "> The input grids contain various colored squares arranged on a black background in different positions. In the transformation, all colored squares \"fall\" vertically to the bottom row while maintaining their relative horizontal positions and original colors. The rest of the grid becomes filled with black squares, resulting in an output where all non-black squares are aligned in the bottom row, preserving their left-to-right ordering from the input grid.\n" 218 | ] 219 | } 220 | ], 221 | "source": [ 222 | "#| echo: false\n", 223 | "#| output: asis\n", 224 | "#| eval: false\n", 225 | "print(f\"> {d[0].d}\")" 226 | ] 227 | }, 228 | { 229 | "cell_type": "markdown", 230 | "metadata": {}, 231 | "source": [ 232 | "::: {.callout-warning}\n", 233 | "Depending on the task and the description strategy used (see [docs](https://agemoai.github.io/arcsolver/describe.html)), `DescriptionGenerator` may decompose the task into multiple images, resulting in a token-intensive prompt (~$0.10 using Sonnet 3.5).\n", 234 | ":::" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "metadata": {}, 240 | "source": [ 241 | "### Solution Generation\n", 242 | "\n", 243 | "Use Claude to construct a solution to an ARC task, automatically refining its attempts based on execution and prediction error feedback." 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [ 251 | { 252 | "name": "stdout", 253 | "output_type": "stream", 254 | "text": [ 255 | "\n", 256 | "Solving task: 1e0a9b12\n", 257 | "Generating descriptions... | Attempts: 0/30 | Best Score: 0.000 | Cost: $0.000\n", 258 | "Starting solution attempts... | Attempts: 0/30 | Best Score: 0.000 | Cost: $0.142\n", 259 | "Generating initial solutions... | Attempts: 0/30 | Best Score: 0.000 | Cost: $0.142\n", 260 | "Testing solutions... | Attempts: 0/30 | Best Score: 0.000 | Cost: $0.231\n", 261 | "Continuing refinement... | Attempts: 2/30 | Best Score: 0.866 | Cost: $0.231\n", 262 | "Refining previous solutions... | Attempts: 2/30 | Best Score: 0.866 | Cost: $0.231\n", 263 | "Testing solutions... | Attempts: 2/30 | Best Score: 0.866 | Cost: $0.332\n", 264 | "Continuing refinement... | Attempts: 4/30 | Best Score: 0.904 | Cost: $0.332\n", 265 | "Refining previous solutions... | Attempts: 4/30 | Best Score: 0.904 | Cost: $0.332\n", 266 | "Testing solutions... | Attempts: 4/30 | Best Score: 0.904 | Cost: $0.424\n", 267 | "Continuing refinement... | Attempts: 6/30 | Best Score: 0.951 | Cost: $0.424\n", 268 | "Refining previous solutions... | Attempts: 6/30 | Best Score: 0.951 | Cost: $0.424\n", 269 | "Testing solutions... | Attempts: 6/30 | Best Score: 0.951 | Cost: $0.528\n", 270 | "Continuing refinement... | Attempts: 8/30 | Best Score: 0.951 | Cost: $0.528\n", 271 | "Refining previous solutions... | Attempts: 8/30 | Best Score: 0.951 | Cost: $0.528\n", 272 | "Testing solutions... | Attempts: 8/30 | Best Score: 0.951 | Cost: $0.633\n", 273 | "Continuing refinement... | Attempts: 10/30 | Best Score: 0.958 | Cost: $0.633\n", 274 | "Refining previous solutions... | Attempts: 10/30 | Best Score: 0.958 | Cost: $0.633\n", 275 | "Testing solutions... | Attempts: 10/30 | Best Score: 0.958 | Cost: $0.732\n", 276 | "Continuing refinement... | Attempts: 12/30 | Best Score: 0.965 | Cost: $0.732\n", 277 | "Refining previous solutions... | Attempts: 12/30 | Best Score: 0.965 | Cost: $0.732\n", 278 | "Testing solutions... | Attempts: 12/30 | Best Score: 0.965 | Cost: $0.835\n", 279 | "Found potential solution, validating... | Attempts: 12/30 | Best Score: 1.000 | Cost: $0.835\n", 280 | "Solution found! | Attempts: 14/30 | Best Score: 1.000 | Cost: $0.835\n", 281 | "Solution found! 🎉 | Attempts: 14/30 | Best Score: 1.000 | Cost: $0.835\n" 282 | ] 283 | } 284 | ], 285 | "source": [ 286 | "#| eval: false\n", 287 | "from arcsolver.solve import ArcSolver\n", 288 | "\n", 289 | "solver = ArcSolver()\n", 290 | "solutions = await solver.solve(task)" 291 | ] 292 | }, 293 | { 294 | "cell_type": "markdown", 295 | "metadata": {}, 296 | "source": [ 297 | "## Contributing\n", 298 | "\n", 299 | "Contributions are welcome! Refined prompts, new OCM primitives, expanded tool-use, alternative retry strategy...\n", 300 | "\n", 301 | "Feel free to raise an issue or submit a PR." 302 | ] 303 | }, 304 | { 305 | "cell_type": "markdown", 306 | "metadata": {}, 307 | "source": [ 308 | "### Learn More\n", 309 | "\n", 310 | "To read about the motivation for building this tool, check out our [blog](https://agemo.ai/resources/summer-of-arc-agi) and watch out for future posts" 311 | ] 312 | }, 313 | { 314 | "cell_type": "code", 315 | "execution_count": null, 316 | "metadata": {}, 317 | "outputs": [], 318 | "source": [] 319 | } 320 | ], 321 | "metadata": { 322 | "kernelspec": { 323 | "display_name": "python3", 324 | "language": "python", 325 | "name": "python3" 326 | } 327 | }, 328 | "nbformat": 4, 329 | "nbformat_minor": 4 330 | } 331 | -------------------------------------------------------------------------------- /nbs/nbdev.yml: -------------------------------------------------------------------------------- 1 | project: 2 | output-dir: _docs 3 | 4 | website: 5 | title: "arcsolver" 6 | site-url: "https://agemoai.github.io/arcsolver" 7 | description: "A Python library for automatically solving Abstraction and Reasoning Corpus (ARC) challenges using Claude and object-centric modeling." 8 | repo-branch: main 9 | repo-url: "https://github.com/agemoai/arcsolver" 10 | -------------------------------------------------------------------------------- /nbs/sidebar.yml: -------------------------------------------------------------------------------- 1 | website: 2 | sidebar: 3 | contents: 4 | - index.ipynb 5 | - 00_task.ipynb 6 | - 01_ocm.ipynb 7 | - 02_describe.ipynb 8 | - 03_solve.ipynb 9 | - 04_utils.ipynb 10 | - 05_examples.ipynb 11 | - 06_score.ipynb 12 | -------------------------------------------------------------------------------- /nbs/styles.css: -------------------------------------------------------------------------------- 1 | .cell { 2 | margin-bottom: 1rem; 3 | } 4 | 5 | .cell > .sourceCode { 6 | margin-bottom: 0; 7 | } 8 | 9 | .cell-output > pre { 10 | margin-bottom: 0; 11 | } 12 | 13 | .cell-output > pre, .cell-output > .sourceCode > pre, .cell-output-stdout > pre { 14 | margin-left: 0.8rem; 15 | margin-top: 0; 16 | background: none; 17 | border-left: 2px solid lightsalmon; 18 | border-top-left-radius: 0; 19 | border-top-right-radius: 0; 20 | } 21 | 22 | .cell-output > .sourceCode { 23 | border: none; 24 | } 25 | 26 | .cell-output > .sourceCode { 27 | background: none; 28 | margin-top: 0; 29 | } 30 | 31 | div.description { 32 | padding-left: 2px; 33 | padding-top: 5px; 34 | font-style: italic; 35 | font-size: 135%; 36 | opacity: 70%; 37 | } 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64.0"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | repo = arcsolver 3 | lib_name = arcsolver 4 | version = 0.0.11 5 | min_python = 3.7 6 | license = apache2 7 | black_formatting = False 8 | doc_path = _docs 9 | lib_path = arcsolver 10 | nbs_path = nbs 11 | recursive = True 12 | tst_flags = notest 13 | put_version_in_init = True 14 | branch = main 15 | custom_sidebar = False 16 | doc_host = https://agemoai.github.io 17 | doc_baseurl = /arcsolver 18 | git_url = https://github.com/agemoai/arcsolver 19 | title = arcsolver 20 | audience = Developers 21 | author = Jack Hogan 22 | author_email = jack@agemo.ai 23 | copyright = 2024 onwards, Jack Hogan 24 | description = A Python library for automatically solving Abstraction and Reasoning Corpus (ARC) challenges using Claude and object-centric modeling. 25 | keywords = nbdev jupyter notebook python 26 | language = English 27 | status = 3 28 | user = agemoai 29 | requirements = fastcore numpy scipy pydantic matplotlib nest_asyncio treelib 30 | readme_nb = index.ipynb 31 | allowed_metadata_keys = 32 | allowed_cell_metadata_keys = 33 | jupyter_hooks = False 34 | clean_ids = True 35 | clear_all = False 36 | cell_number = True 37 | skip_procs = 38 | 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import parse_version 2 | from configparser import ConfigParser 3 | import setuptools, shlex 4 | assert parse_version(setuptools.__version__)>=parse_version('36.2') 5 | 6 | # note: all settings are in settings.ini; edit there, not here 7 | config = ConfigParser(delimiters=['=']) 8 | config.read('settings.ini', encoding='utf-8') 9 | cfg = config['DEFAULT'] 10 | 11 | cfg_keys = 'version description keywords author author_email'.split() 12 | expected = cfg_keys + "lib_name user branch license status min_python audience language".split() 13 | for o in expected: assert o in cfg, "missing expected setting: {}".format(o) 14 | setup_cfg = {o:cfg[o] for o in cfg_keys} 15 | 16 | licenses = { 17 | 'apache2': ('Apache Software License 2.0','OSI Approved :: Apache Software License'), 18 | 'mit': ('MIT License', 'OSI Approved :: MIT License'), 19 | 'gpl2': ('GNU General Public License v2', 'OSI Approved :: GNU General Public License v2 (GPLv2)'), 20 | 'gpl3': ('GNU General Public License v3', 'OSI Approved :: GNU General Public License v3 (GPLv3)'), 21 | 'bsd3': ('BSD License', 'OSI Approved :: BSD License'), 22 | } 23 | statuses = [ '1 - Planning', '2 - Pre-Alpha', '3 - Alpha', 24 | '4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ] 25 | py_versions = '3.6 3.7 3.8 3.9 3.10 3.11 3.12'.split() 26 | 27 | requirements = shlex.split(cfg.get('requirements', '')) 28 | if cfg.get('pip_requirements'): requirements += shlex.split(cfg.get('pip_requirements', '')) 29 | min_python = cfg['min_python'] 30 | lic = licenses.get(cfg['license'].lower(), (cfg['license'], None)) 31 | dev_requirements = (cfg.get('dev_requirements') or '').split() 32 | 33 | package_data = dict() 34 | pkg_data = cfg.get('package_data', None) 35 | if pkg_data: 36 | package_data[cfg['lib_name']] = pkg_data.split() # split as multiple files might be listed 37 | # Add package data to setup_cfg for setuptools.setup(..., **setup_cfg) 38 | setup_cfg['package_data'] = package_data 39 | 40 | setuptools.setup( 41 | name = cfg['lib_name'], 42 | license = lic[0], 43 | classifiers = [ 44 | 'Development Status :: ' + statuses[int(cfg['status'])], 45 | 'Intended Audience :: ' + cfg['audience'].title(), 46 | 'Natural Language :: ' + cfg['language'].title(), 47 | ] + ['Programming Language :: Python :: '+o for o in py_versions[py_versions.index(min_python):]] + (['License :: ' + lic[1] ] if lic[1] else []), 48 | url = cfg['git_url'], 49 | packages = setuptools.find_packages(), 50 | include_package_data = True, 51 | install_requires = requirements, 52 | extras_require={ 'dev': dev_requirements }, 53 | dependency_links = cfg.get('dep_links','').split(), 54 | python_requires = '>=' + cfg['min_python'], 55 | long_description = open('README.md', encoding='utf-8').read(), 56 | long_description_content_type = 'text/markdown', 57 | zip_safe = False, 58 | entry_points = { 59 | 'console_scripts': cfg.get('console_scripts','').split(), 60 | 'nbdev': [f'{cfg.get("lib_path")}={cfg.get("lib_path")}._modidx:d'] 61 | }, 62 | **setup_cfg) 63 | 64 | 65 | --------------------------------------------------------------------------------