guohanghui commited on
Commit
6c0c5bc
·
verified ·
1 Parent(s): d70f3ac

Upload 350 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +5 -0
  2. Dockerfile +18 -0
  3. README.md +27 -5
  4. app.py +45 -0
  5. gala/mcp_output/README_MCP.md +45 -0
  6. gala/mcp_output/analysis.json +670 -0
  7. gala/mcp_output/diff_report.md +61 -0
  8. gala/mcp_output/mcp_plugin/__init__.py +0 -0
  9. gala/mcp_output/mcp_plugin/adapter.py +142 -0
  10. gala/mcp_output/mcp_plugin/main.py +13 -0
  11. gala/mcp_output/mcp_plugin/mcp_service.py +51 -0
  12. gala/mcp_output/requirements.txt +9 -0
  13. gala/mcp_output/start_mcp.py +30 -0
  14. gala/mcp_output/workflow_summary.json +201 -0
  15. gala/source/.clang-format +1 -0
  16. gala/source/.pre-commit-config.yaml +65 -0
  17. gala/source/.prettierrc.toml +1 -0
  18. gala/source/.readthedocs.yml +28 -0
  19. gala/source/AUTHORS.rst +22 -0
  20. gala/source/CHANGES.rst +725 -0
  21. gala/source/CODE_OF_CONDUCT.md +2 -0
  22. gala/source/LICENSE +21 -0
  23. gala/source/README.rst +98 -0
  24. gala/source/__init__.py +4 -0
  25. gala/source/codemeta.json +23 -0
  26. gala/source/conftest.py +41 -0
  27. gala/source/docs/Makefile +149 -0
  28. gala/source/docs/_static/Gala_Logo_RGB.png +0 -0
  29. gala/source/docs/_static/anim-prof.mp4 +3 -0
  30. gala/source/docs/_static/gala.css +70 -0
  31. gala/source/docs/_static/m104.ico +0 -0
  32. gala/source/docs/_static/orbit-anim1.mp4 +3 -0
  33. gala/source/docs/_static/orbit-anim2.mp4 +3 -0
  34. gala/source/docs/_static_animations.py +31 -0
  35. gala/source/docs/_templates/autosummary/base.rst +10 -0
  36. gala/source/docs/_templates/autosummary/class.rst +65 -0
  37. gala/source/docs/_templates/autosummary/module.rst +41 -0
  38. gala/source/docs/conf.py +356 -0
  39. gala/source/docs/contributing.rst +19 -0
  40. gala/source/docs/conventions.rst +49 -0
  41. gala/source/docs/coordinates/greatcircle.rst +184 -0
  42. gala/source/docs/coordinates/index.rst +241 -0
  43. gala/source/docs/docs.rst +16 -0
  44. gala/source/docs/dynamics/actionangle.rst +501 -0
  45. gala/source/docs/dynamics/index.rst +172 -0
  46. gala/source/docs/dynamics/mockstreams.rst +313 -0
  47. gala/source/docs/dynamics/nbody.rst +184 -0
  48. gala/source/docs/dynamics/nd-representations.rst +69 -0
  49. gala/source/docs/dynamics/nonlinear.rst +164 -0
  50. gala/source/docs/dynamics/orbits-in-detail.rst +343 -0
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ gala/source/docs/_static/anim-prof.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ gala/source/docs/_static/orbit-anim1.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ gala/source/docs/_static/orbit-anim2.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ gala/source/docs/tutorials/data/m12m.cache filter=lfs diff=lfs merge=lfs -text
40
+ gala/source/tests/potential/potential/EXP-Hernquist.cache filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+ ENV MCP_TRANSPORT=http
14
+ ENV MCP_PORT=7860
15
+
16
+ EXPOSE 7860
17
+
18
+ CMD ["python", "gala/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,32 @@
1
  ---
2
- title: Gala
3
- emoji: 🐢
4
- colorFrom: gray
5
- colorTo: indigo
6
  sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Gala MCP
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ sdk_version: "4.26.0"
8
+ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # Gala MCP Service
13
+
14
+ Auto-generated MCP service for gala.
15
+
16
+ ## Usage
17
+
18
+ ```
19
+ https://None-gala-mcp.hf.space/mcp
20
+ ```
21
+
22
+ ## Connect with Cursor
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "gala": {
28
+ "url": "https://None-gala-mcp.hf.space/mcp"
29
+ }
30
+ }
31
+ }
32
+ ```
app.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ import os
3
+ import sys
4
+
5
+ mcp_plugin_path = os.path.join(os.path.dirname(__file__), "gala", "mcp_output", "mcp_plugin")
6
+ sys.path.insert(0, mcp_plugin_path)
7
+
8
+ app = FastAPI(
9
+ title="Gala MCP Service",
10
+ description="Auto-generated MCP service for gala",
11
+ version="1.0.0"
12
+ )
13
+
14
+ @app.get("/")
15
+ def root():
16
+ return {
17
+ "service": "Gala MCP Service",
18
+ "version": "1.0.0",
19
+ "status": "running",
20
+ "transport": os.environ.get("MCP_TRANSPORT", "http")
21
+ }
22
+
23
+ @app.get("/health")
24
+ def health_check():
25
+ return {"status": "healthy", "service": "gala MCP"}
26
+
27
+ @app.get("/tools")
28
+ def list_tools():
29
+ try:
30
+ from mcp_service import create_app
31
+ mcp_app = create_app()
32
+ tools = []
33
+ for tool_name, tool_func in mcp_app.tools.items():
34
+ tools.append({
35
+ "name": tool_name,
36
+ "description": tool_func.__doc__ or "No description available"
37
+ })
38
+ return {"tools": tools}
39
+ except Exception as e:
40
+ return {"error": f"Failed to load tools: {str(e)}"}
41
+
42
+ if __name__ == "__main__":
43
+ import uvicorn
44
+ port = int(os.environ.get("PORT", 7860))
45
+ uvicorn.run(app, host="0.0.0.0", port=port)
gala/mcp_output/README_MCP.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gala: Galactic and Gravitational Dynamics in Python
2
+
3
+ ## Project Introduction
4
+
5
+ Gala is a Python library designed for galactic and gravitational dynamics. It provides tools for handling stream coordinates, core dynamics functions, and potential functions. The library is structured to facilitate the study and simulation of astrophysical systems, offering a comprehensive suite of functions and classes for researchers and developers in the field.
6
+
7
+ ## Installation Method
8
+
9
+ To install Gala, ensure you have Python installed along with the following required dependencies: `numpy`, `scipy`, and `astropy`. Optionally, you can install `matplotlib` for plotting capabilities.
10
+
11
+ You can install Gala using pip:
12
+
13
+ ```
14
+ pip install gala
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ Here's a quick example of how to use Gala's main functions:
20
+
21
+ 1. Import the necessary modules:
22
+
23
+ ```
24
+ from gala.coordinates import gd1
25
+ from gala.dynamics import core
26
+ from gala.potential import builtin
27
+ ```
28
+
29
+ 2. Use the functions and classes provided by these modules to perform your desired calculations. For example, you can handle GD1 stream coordinates, utilize core dynamics functions, or work with potential functions.
30
+
31
+ ## Available Tools and Endpoints List
32
+
33
+ - **GD1 Stream Coordinates**: Module for handling GD1 stream coordinates.
34
+ - **Core Dynamics Functions**: Provides core functions and classes for dynamics calculations.
35
+ - **Core Potential Functions**: Offers core functions and classes for potential calculations.
36
+
37
+ ## Common Issues and Notes
38
+
39
+ - **Dependencies**: Ensure all required dependencies (`numpy`, `scipy`, `astropy`) are installed. Optional dependency `matplotlib` is recommended for visualization.
40
+ - **Environment**: It is advisable to use a virtual environment to manage dependencies and avoid conflicts.
41
+ - **Performance**: For optimal performance, ensure your Python environment is up-to-date and consider using optimized numerical libraries.
42
+
43
+ ## Reference Links or Documentation
44
+
45
+ For more detailed information, visit the [Gala GitHub Repository](https://github.com/adrn/gala). Here you can find comprehensive documentation, tutorials, and additional resources to help you get the most out of Gala.
gala/mcp_output/analysis.json ADDED
@@ -0,0 +1,670 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "summary": {
3
+ "repository_url": "https://github.com/adrn/gala",
4
+ "summary": "Imported via zip fallback, file count: 187",
5
+ "file_tree": {
6
+ ".github/actions/build-exp/action.yml": {
7
+ "size": 2168
8
+ },
9
+ ".github/dependabot.yml": {
10
+ "size": 118
11
+ },
12
+ ".github/pull_request_template.md": {
13
+ "size": 279
14
+ },
15
+ ".github/workflows/benchmarks.yml": {
16
+ "size": 3185
17
+ },
18
+ ".github/workflows/tests.yml": {
19
+ "size": 7580
20
+ },
21
+ ".github/workflows/tutorials.yml": {
22
+ "size": 2038
23
+ },
24
+ ".github/workflows/wheels.yml": {
25
+ "size": 3533
26
+ },
27
+ ".pre-commit-config.yaml": {
28
+ "size": 1807
29
+ },
30
+ ".prettierrc.toml": {
31
+ "size": 16
32
+ },
33
+ ".readthedocs.yml": {
34
+ "size": 545
35
+ },
36
+ "CODE_OF_CONDUCT.md": {
37
+ "size": 138
38
+ },
39
+ "codemeta.json": {
40
+ "size": 1411
41
+ },
42
+ "conftest.py": {
43
+ "size": 1007
44
+ },
45
+ "docs/_static_animations.py": {
46
+ "size": 924
47
+ },
48
+ "docs/conf.py": {
49
+ "size": 10780
50
+ },
51
+ "docs/dynamics/references.txt": {
52
+ "size": 258
53
+ },
54
+ "docs/references.txt": {
55
+ "size": 283
56
+ },
57
+ "docs/supporting/data/Eilers2019-circ-velocity.txt": {
58
+ "size": 888
59
+ },
60
+ "docs/supporting/define-milky-way-model.py": {
61
+ "size": 11944
62
+ },
63
+ "docs/tutorials/Arbitrary-density-SCF.py": {
64
+ "size": 8116
65
+ },
66
+ "docs/tutorials/Milky-Way-model.py": {
67
+ "size": 6344
68
+ },
69
+ "docs/tutorials/data/m12m-basis.yml": {
70
+ "size": 151
71
+ },
72
+ "docs/tutorials/integrate-barred-potential.py": {
73
+ "size": 11556
74
+ },
75
+ "docs/tutorials/pyia-gala-orbit.py": {
76
+ "size": 8636
77
+ },
78
+ "docs/tutorials/spherical-spline-tutorial.py": {
79
+ "size": 13882
80
+ },
81
+ "docs/tutorials/stream-mass-loss.py": {
82
+ "size": 8985
83
+ },
84
+ "docs/tutorials/time-evolving-potential.py": {
85
+ "size": 12151
86
+ },
87
+ "docs/tutorials/v1_11_new_features.py": {
88
+ "size": 11698
89
+ },
90
+ "paper/paper.md": {
91
+ "size": 2603
92
+ },
93
+ "pyproject.toml": {
94
+ "size": 6060
95
+ },
96
+ "setup.py": {
97
+ "size": 17809
98
+ },
99
+ "src/gala/__init__.py": {
100
+ "size": 115
101
+ },
102
+ "src/gala/_compat_utils.py": {
103
+ "size": 295
104
+ },
105
+ "src/gala/_optional_deps.py": {
106
+ "size": 1326
107
+ },
108
+ "src/gala/coordinates/__init__.py": {
109
+ "size": 326
110
+ },
111
+ "src/gala/coordinates/gd1.py": {
112
+ "size": 3634
113
+ },
114
+ "src/gala/coordinates/greatcircle.py": {
115
+ "size": 17915
116
+ },
117
+ "src/gala/coordinates/helpers.py": {
118
+ "size": 1610
119
+ },
120
+ "src/gala/coordinates/jhelum.py": {
121
+ "size": 3551
122
+ },
123
+ "src/gala/coordinates/magellanic_stream.py": {
124
+ "size": 3105
125
+ },
126
+ "src/gala/coordinates/oph.py": {
127
+ "size": 3710
128
+ },
129
+ "src/gala/coordinates/orphan.py": {
130
+ "size": 6635
131
+ },
132
+ "src/gala/coordinates/pal13.py": {
133
+ "size": 3592
134
+ },
135
+ "src/gala/coordinates/pal5.py": {
136
+ "size": 3891
137
+ },
138
+ "src/gala/coordinates/pm_cov_transform.py": {
139
+ "size": 4435
140
+ },
141
+ "src/gala/coordinates/poincarepolar.py": {
142
+ "size": 1316
143
+ },
144
+ "src/gala/coordinates/reflex.py": {
145
+ "size": 1661
146
+ },
147
+ "src/gala/coordinates/sgr.py": {
148
+ "size": 7378
149
+ },
150
+ "src/gala/coordinates/velocity_frame_transforms.py": {
151
+ "size": 2288
152
+ },
153
+ "src/gala/dynamics/__init__.py": {
154
+ "size": 234
155
+ },
156
+ "src/gala/dynamics/actionangle/__init__.py": {
157
+ "size": 103
158
+ },
159
+ "src/gala/dynamics/actionangle/actionangle_o2gf.py": {
160
+ "size": 23921
161
+ },
162
+ "src/gala/dynamics/actionangle/actionangle_staeckel.py": {
163
+ "size": 3154
164
+ },
165
+ "src/gala/dynamics/actionangle/analyticactionangle.py": {
166
+ "size": 11871
167
+ },
168
+ "src/gala/dynamics/core.py": {
169
+ "size": 32983
170
+ },
171
+ "src/gala/dynamics/lyapunov/__init__.py": {
172
+ "size": 80
173
+ },
174
+ "src/gala/dynamics/mockstream/__init__.py": {
175
+ "size": 117
176
+ },
177
+ "src/gala/dynamics/mockstream/core.py": {
178
+ "size": 5509
179
+ },
180
+ "src/gala/dynamics/mockstream/mockstream_generator.py": {
181
+ "size": 15603
182
+ },
183
+ "src/gala/dynamics/nbody/__init__.py": {
184
+ "size": 30
185
+ },
186
+ "src/gala/dynamics/nbody/core.py": {
187
+ "size": 10060
188
+ },
189
+ "src/gala/dynamics/nonlinear.py": {
190
+ "size": 10325
191
+ },
192
+ "src/gala/dynamics/orbit.py": {
193
+ "size": 48470
194
+ },
195
+ "src/gala/dynamics/plot.py": {
196
+ "size": 4619
197
+ },
198
+ "src/gala/dynamics/representation_nd.py": {
199
+ "size": 8785
200
+ },
201
+ "src/gala/dynamics/util.py": {
202
+ "size": 11912
203
+ },
204
+ "src/gala/integrate/__init__.py": {
205
+ "size": 75
206
+ },
207
+ "src/gala/integrate/core.py": {
208
+ "size": 6343
209
+ },
210
+ "src/gala/integrate/cyintegrators/__init__.py": {
211
+ "size": 149
212
+ },
213
+ "src/gala/integrate/cyintegrators/dopri/__init__.py": {
214
+ "size": 0
215
+ },
216
+ "src/gala/integrate/cyintegrators/dopri/licence.txt": {
217
+ "size": 1258
218
+ },
219
+ "src/gala/integrate/lookup.py": {
220
+ "size": 2646
221
+ },
222
+ "src/gala/integrate/pyintegrators/__init__.py": {
223
+ "size": 148
224
+ },
225
+ "src/gala/integrate/pyintegrators/dopri853.py": {
226
+ "size": 5726
227
+ },
228
+ "src/gala/integrate/pyintegrators/leapfrog.py": {
229
+ "size": 6603
230
+ },
231
+ "src/gala/integrate/pyintegrators/rk5.py": {
232
+ "size": 6177
233
+ },
234
+ "src/gala/integrate/pyintegrators/ruth4.py": {
235
+ "size": 4903
236
+ },
237
+ "src/gala/integrate/timespec.py": {
238
+ "size": 4550
239
+ },
240
+ "src/gala/io.py": {
241
+ "size": 1176
242
+ },
243
+ "src/gala/logging.py": {
244
+ "size": 946
245
+ },
246
+ "src/gala/potential/__init__.py": {
247
+ "size": 142
248
+ },
249
+ "src/gala/potential/common.py": {
250
+ "size": 11179
251
+ },
252
+ "src/gala/potential/frame/__init__.py": {
253
+ "size": 82
254
+ },
255
+ "src/gala/potential/frame/builtin/__init__.py": {
256
+ "size": 55
257
+ },
258
+ "src/gala/potential/frame/builtin/transformations.py": {
259
+ "size": 7285
260
+ },
261
+ "src/gala/potential/frame/core.py": {
262
+ "size": 692
263
+ },
264
+ "src/gala/potential/hamiltonian/__init__.py": {
265
+ "size": 28
266
+ },
267
+ "src/gala/potential/potential/__init__.py": {
268
+ "size": 542
269
+ },
270
+ "src/gala/potential/potential/builtin/__init__.py": {
271
+ "size": 68
272
+ },
273
+ "src/gala/potential/potential/builtin/core.py": {
274
+ "size": 59261
275
+ },
276
+ "src/gala/potential/potential/builtin/pybuiltin.py": {
277
+ "size": 3319
278
+ },
279
+ "src/gala/potential/potential/builtin/special.py": {
280
+ "size": 13787
281
+ },
282
+ "src/gala/potential/potential/builtin/time_interpolated.py": {
283
+ "size": 16390
284
+ },
285
+ "src/gala/potential/potential/core.py": {
286
+ "size": 55457
287
+ },
288
+ "src/gala/potential/potential/interop.py": {
289
+ "size": 15067
290
+ },
291
+ "src/gala/potential/potential/io.py": {
292
+ "size": 10811
293
+ },
294
+ "src/gala/potential/potential/symmetry.py": {
295
+ "size": 9532
296
+ },
297
+ "src/gala/potential/potential/util.py": {
298
+ "size": 7088
299
+ },
300
+ "src/gala/potential/scf/__init__.py": {
301
+ "size": 198
302
+ },
303
+ "src/gala/potential/scf/core.py": {
304
+ "size": 8733
305
+ },
306
+ "src/gala/units.py": {
307
+ "size": 12239
308
+ },
309
+ "src/gala/util.py": {
310
+ "size": 4310
311
+ },
312
+ "tests/benchmarks/test_integrate_benchmark.py": {
313
+ "size": 1163
314
+ },
315
+ "tests/benchmarks/test_mockstream_benchmark.py": {
316
+ "size": 4980
317
+ },
318
+ "tests/benchmarks/test_potentials_benchmark.py": {
319
+ "size": 3890
320
+ },
321
+ "tests/coordinates/gd1_coord.txt": {
322
+ "size": 868
323
+ },
324
+ "tests/coordinates/idl_vgsr_vhel.txt": {
325
+ "size": 12419
326
+ },
327
+ "tests/coordinates/sergey_orphan.txt": {
328
+ "size": 10109
329
+ },
330
+ "tests/coordinates/test_all_streamframes.py": {
331
+ "size": 1069
332
+ },
333
+ "tests/coordinates/test_gd1.py": {
334
+ "size": 1735
335
+ },
336
+ "tests/coordinates/test_greatcircle.py": {
337
+ "size": 7929
338
+ },
339
+ "tests/coordinates/test_jhelum.py": {
340
+ "size": 956
341
+ },
342
+ "tests/coordinates/test_orphan.py": {
343
+ "size": 1560
344
+ },
345
+ "tests/coordinates/test_pal5.py": {
346
+ "size": 974
347
+ },
348
+ "tests/coordinates/test_pm_cov_transform.py": {
349
+ "size": 2938
350
+ },
351
+ "tests/coordinates/test_reflex.py": {
352
+ "size": 1771
353
+ },
354
+ "tests/coordinates/test_sgr.py": {
355
+ "size": 2292
356
+ },
357
+ "tests/coordinates/test_velocity_frame_transforms.py": {
358
+ "size": 3193
359
+ },
360
+ "tests/dynamics/actionangle/_genfunc/__init__.py": {
361
+ "size": 0
362
+ },
363
+ "tests/dynamics/actionangle/_genfunc/genfunc_3d.py": {
364
+ "size": 16639
365
+ },
366
+ "tests/dynamics/actionangle/_genfunc/solver.py": {
367
+ "size": 6039
368
+ },
369
+ "tests/dynamics/actionangle/_genfunc/test_potentials.py": {
370
+ "size": 9660
371
+ },
372
+ "tests/dynamics/actionangle/_genfunc/toy_potentials.py": {
373
+ "size": 5430
374
+ },
375
+ "tests/dynamics/actionangle/_genfunc/visualize_surfaces.py": {
376
+ "size": 2977
377
+ },
378
+ "tests/dynamics/actionangle/actionangle_helpers.py": {
379
+ "size": 4709
380
+ },
381
+ "tests/dynamics/actionangle/staeckel_helpers.py": {
382
+ "size": 2978
383
+ },
384
+ "tests/dynamics/actionangle/test_actionangle_o2gf.py": {
385
+ "size": 8465
386
+ },
387
+ "tests/dynamics/actionangle/test_actionangle_staeckel.py": {
388
+ "size": 5114
389
+ },
390
+ "tests/dynamics/actionangle/test_analyticactionangle.py": {
391
+ "size": 6117
392
+ },
393
+ "tests/dynamics/mockstream/test_coord.py": {
394
+ "size": 593
395
+ },
396
+ "tests/dynamics/mockstream/test_df.py": {
397
+ "size": 4083
398
+ },
399
+ "tests/dynamics/mockstream/test_mockstream.py": {
400
+ "size": 27377
401
+ },
402
+ "tests/dynamics/mockstream/test_mockstream_class.py": {
403
+ "size": 4377
404
+ },
405
+ "tests/dynamics/nbody/test_nbody.py": {
406
+ "size": 9203
407
+ },
408
+ "tests/dynamics/test_dynamics_core.py": {
409
+ "size": 14198
410
+ },
411
+ "tests/dynamics/test_dynamics_util.py": {
412
+ "size": 3802
413
+ },
414
+ "tests/dynamics/test_nonlinear.py": {
415
+ "size": 10282
416
+ },
417
+ "tests/dynamics/test_orbit.py": {
418
+ "size": 20176
419
+ },
420
+ "tests/dynamics/test_plot.py": {
421
+ "size": 4553
422
+ },
423
+ "tests/dynamics/test_representation_nd.py": {
424
+ "size": 1537
425
+ },
426
+ "tests/integrate/__init__.py": {
427
+ "size": 0
428
+ },
429
+ "tests/integrate/test_cyintegrators.py": {
430
+ "size": 4477
431
+ },
432
+ "tests/integrate/test_pyintegrators.py": {
433
+ "size": 4395
434
+ },
435
+ "tests/integrate/test_timespec.py": {
436
+ "size": 2313
437
+ },
438
+ "tests/integration/README.md": {
439
+ "size": 178
440
+ },
441
+ "tests/integration/test_bar_rotating_frame.py": {
442
+ "size": 7655
443
+ },
444
+ "tests/potential/frame/test_builtin.py": {
445
+ "size": 2600
446
+ },
447
+ "tests/potential/frame/test_transformations.py": {
448
+ "size": 3544
449
+ },
450
+ "tests/potential/hamiltonian/hamiltonian_helpers.py": {
451
+ "size": 6076
452
+ },
453
+ "tests/potential/hamiltonian/test_hamiltonian.py": {
454
+ "size": 1935
455
+ },
456
+ "tests/potential/hamiltonian/test_with_frame_potential.py": {
457
+ "size": 6767
458
+ },
459
+ "tests/potential/potential/Composite.yml": {
460
+ "size": 531
461
+ },
462
+ "tests/potential/potential/EXP-Hernquist-basis.yml": {
463
+ "size": 163
464
+ },
465
+ "tests/potential/potential/EXP-field-basis.yml": {
466
+ "size": 85
467
+ },
468
+ "tests/potential/potential/HarmonicOscillator1D.yml": {
469
+ "size": 60
470
+ },
471
+ "tests/potential/potential/Plummer.yml": {
472
+ "size": 184
473
+ },
474
+ "tests/potential/potential/ccomposite.yml": {
475
+ "size": 690
476
+ },
477
+ "tests/potential/potential/exp_basis.yml": {
478
+ "size": 157
479
+ },
480
+ "tests/potential/potential/generate_agama.py": {
481
+ "size": 983
482
+ },
483
+ "tests/potential/potential/generate_exp.py": {
484
+ "size": 7015
485
+ },
486
+ "tests/potential/potential/lm10.yml": {
487
+ "size": 1284
488
+ },
489
+ "tests/potential/potential/potential_helpers.py": {
490
+ "size": 20993
491
+ },
492
+ "tests/potential/potential/test_all_builtin.py": {
493
+ "size": 21079
494
+ },
495
+ "tests/potential/potential/test_composite.py": {
496
+ "size": 6171
497
+ },
498
+ "tests/potential/potential/test_cpotential.py": {
499
+ "size": 621
500
+ },
501
+ "tests/potential/potential/test_exp.py": {
502
+ "size": 21742
503
+ },
504
+ "tests/potential/potential/test_interop_agama.py": {
505
+ "size": 3457
506
+ },
507
+ "tests/potential/potential/test_interop_galpy.py": {
508
+ "size": 6895
509
+ },
510
+ "tests/potential/potential/test_io.py": {
511
+ "size": 4199
512
+ },
513
+ "tests/potential/potential/test_potential_core.py": {
514
+ "size": 5222
515
+ },
516
+ "tests/potential/potential/test_potential_util.py": {
517
+ "size": 1769
518
+ },
519
+ "tests/potential/potential/test_special.py": {
520
+ "size": 1361
521
+ },
522
+ "tests/potential/potential/test_spherical_spline.py": {
523
+ "size": 5832
524
+ },
525
+ "tests/potential/potential/test_symmetry.py": {
526
+ "size": 24931
527
+ },
528
+ "tests/potential/potential/test_time_interpolated.py": {
529
+ "size": 17566
530
+ },
531
+ "tests/potential/scf/data/README.md": {
532
+ "size": 87
533
+ },
534
+ "tests/potential/scf/data/plummer_coeff_nmax10_lmax5.txt": {
535
+ "size": 19991
536
+ },
537
+ "tests/potential/scf/data/plummer_coeff_var_nmax10_lmax5.txt": {
538
+ "size": 29802
539
+ },
540
+ "tests/potential/scf/test_accp_fortran.py": {
541
+ "size": 4560
542
+ },
543
+ "tests/potential/scf/test_bfe.py": {
544
+ "size": 6941
545
+ },
546
+ "tests/potential/scf/test_bfe_interp.py": {
547
+ "size": 1435
548
+ },
549
+ "tests/potential/scf/test_class.py": {
550
+ "size": 3451
551
+ },
552
+ "tests/potential/scf/test_computecoeff.py": {
553
+ "size": 8489
554
+ },
555
+ "tests/potential/scf/test_computecoeff_discrete.py": {
556
+ "size": 2725
557
+ },
558
+ "tests/potential/scf/test_computecoeff_fortran.py": {
559
+ "size": 1562
560
+ },
561
+ "tests/regression/test_potential_timeinterpolated_539.py": {
562
+ "size": 734
563
+ },
564
+ "tests/test_units.py": {
565
+ "size": 3085
566
+ }
567
+ },
568
+ "processed_by": "zip_fallback",
569
+ "success": true
570
+ },
571
+ "structure": {
572
+ "packages": [
573
+ "source.src.gala",
574
+ "source.tests.integrate"
575
+ ]
576
+ },
577
+ "dependencies": {
578
+ "has_environment_yml": false,
579
+ "has_requirements_txt": false,
580
+ "pyproject": true,
581
+ "setup_cfg": false,
582
+ "setup_py": true
583
+ },
584
+ "entry_points": {
585
+ "imports": [],
586
+ "cli": [],
587
+ "modules": []
588
+ },
589
+ "llm_analysis": {
590
+ "core_modules": [
591
+ {
592
+ "package": "source.src.gala.coordinates",
593
+ "module": "gd1",
594
+ "functions": [
595
+ "function1",
596
+ "function2"
597
+ ],
598
+ "classes": [
599
+ "Class1",
600
+ "Class2"
601
+ ],
602
+ "description": "Module for handling GD1 stream coordinates."
603
+ },
604
+ {
605
+ "package": "source.src.gala.dynamics",
606
+ "module": "core",
607
+ "functions": [
608
+ "function1",
609
+ "function2"
610
+ ],
611
+ "classes": [
612
+ "Class1",
613
+ "Class2"
614
+ ],
615
+ "description": "Core dynamics functions and classes."
616
+ },
617
+ {
618
+ "package": "source.src.gala.potential",
619
+ "module": "builtin.core",
620
+ "functions": [
621
+ "function1",
622
+ "function2"
623
+ ],
624
+ "classes": [
625
+ "Class1",
626
+ "Class2"
627
+ ],
628
+ "description": "Core potential functions and classes."
629
+ }
630
+ ],
631
+ "cli_commands": [],
632
+ "import_strategy": {
633
+ "primary": "import",
634
+ "fallback": "blackbox",
635
+ "confidence": 0.85
636
+ },
637
+ "dependencies": {
638
+ "required": [
639
+ "numpy",
640
+ "scipy",
641
+ "astropy"
642
+ ],
643
+ "optional": [
644
+ "matplotlib"
645
+ ]
646
+ },
647
+ "risk_assessment": {
648
+ "import_feasibility": 0.8,
649
+ "intrusiveness_risk": "medium",
650
+ "complexity": "medium"
651
+ }
652
+ },
653
+ "deepwiki_analysis": {
654
+ "repo_url": "https://github.com/adrn/gala",
655
+ "repo_name": "gala",
656
+ "content": "Galactic and gravitational dynamics in Python\nRepository Not Indexed\nThis repository hasn't been indexed yet. Indexing allows you to explore code structure, find documentation, and understand dependencies.\nIndexing typically takes 2-10 minutes to complete after it starts indexing\nOnce indexed, you'll have full access to code exploration and search functionality",
657
+ "model": "gpt-4o-2024-08-06",
658
+ "source": "selenium",
659
+ "success": true
660
+ },
661
+ "deepwiki_options": {
662
+ "enabled": true,
663
+ "model": "gpt-4o-2024-08-06"
664
+ },
665
+ "risk": {
666
+ "import_feasibility": 0.8,
667
+ "intrusiveness_risk": "medium",
668
+ "complexity": "medium"
669
+ }
670
+ }
gala/mcp_output/diff_report.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Difference Report for Gala Project
2
+
3
+ **Repository:** Gala
4
+ **Project Type:** Python Library
5
+ **Main Features:** Basic Functionality
6
+ **Report Generated On:** 2026-02-04 20:42:09
7
+ **Intrusiveness:** None
8
+ **Workflow Status:** Success
9
+ **Test Status:** Failed
10
+
11
+ ## Project Overview
12
+
13
+ The Gala project is a Python library designed to provide basic functionality for its users. As of the latest update, the project has seen the addition of new files, but no modifications to existing files. The workflow has been successfully executed, but the test status indicates failures, suggesting issues that need to be addressed.
14
+
15
+ ## Difference Analysis
16
+
17
+ ### New Files Added
18
+
19
+ - A total of 8 new files have been introduced to the repository. These files likely contain new features or enhancements to the existing functionality of the library.
20
+
21
+ ### Modified Files
22
+
23
+ - There have been no modifications to existing files, indicating that the recent changes are entirely encapsulated within the newly added files.
24
+
25
+ ## Technical Analysis
26
+
27
+ ### Workflow Status
28
+
29
+ - The workflow status is marked as successful, indicating that the integration and deployment processes were executed without any errors. This suggests that the new files were correctly integrated into the existing project structure.
30
+
31
+ ### Test Status
32
+
33
+ - The test status is marked as failed. This indicates that one or more tests did not pass, which could be due to issues in the newly added files or their integration with the existing codebase.
34
+
35
+ ## Recommendations and Improvements
36
+
37
+ 1. **Review New Files:** Conduct a thorough review of the newly added files to identify any potential issues or bugs that could be causing the test failures.
38
+
39
+ 2. **Enhance Testing:** Improve the test coverage for the new features to ensure that all edge cases are considered and that the new functionality integrates seamlessly with the existing codebase.
40
+
41
+ 3. **Debugging:** Utilize debugging tools to trace the source of the test failures and address any identified issues.
42
+
43
+ 4. **Documentation:** Update the project documentation to reflect the new features and any changes in the usage or API of the library.
44
+
45
+ ## Deployment Information
46
+
47
+ - As the workflow status is successful, the new changes have been deployed. However, given the test failures, it is advisable to hold off on any further deployments until the issues are resolved.
48
+
49
+ ## Future Planning
50
+
51
+ 1. **Issue Resolution:** Prioritize resolving the current test failures to ensure the stability and reliability of the library.
52
+
53
+ 2. **Feature Expansion:** Once the current issues are resolved, consider expanding the library's functionality based on user feedback and project goals.
54
+
55
+ 3. **Community Engagement:** Engage with the user community to gather feedback on the new features and identify areas for improvement.
56
+
57
+ 4. **Regular Updates:** Plan for regular updates and maintenance to keep the library up-to-date with the latest Python standards and practices.
58
+
59
+ ## Conclusion
60
+
61
+ The Gala project has seen significant additions with the introduction of new files. While the integration process was successful, the test failures highlight the need for further investigation and resolution. By addressing these issues and enhancing the testing framework, the project can continue to evolve and provide valuable functionality to its users.
gala/mcp_output/mcp_plugin/__init__.py ADDED
File without changes
gala/mcp_output/mcp_plugin/adapter.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Path settings
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ sys.path.insert(0, source_path)
7
+
8
+ # Import statements
9
+ try:
10
+ from src.gala.dynamics.core import DynamicsCore
11
+ from src.gala.potential.potential.core import PotentialCore
12
+ from src.gala.integrate.core import IntegrateCore
13
+ from src.gala.coordinates.greatcircle import GreatCircle
14
+ from src.gala.dynamics.orbit import Orbit
15
+ from src.gala.potential.potential.builtin.core import BuiltinCore
16
+ except ImportError as e:
17
+ print(f"Import failed: {e}. Ensure the source directory is correctly set.")
18
+
19
+ class Adapter:
20
+ """
21
+ Adapter class for MCP plugin, utilizing the gala library.
22
+ """
23
+
24
+ def __init__(self):
25
+ self.mode = "import"
26
+
27
+ # Dynamics Module
28
+ # -------------------------------------------------------------------------
29
+ def create_dynamics_core_instance(self, *args, **kwargs):
30
+ """
31
+ Create an instance of DynamicsCore.
32
+
33
+ Parameters:
34
+ *args, **kwargs: Arguments for DynamicsCore initialization.
35
+
36
+ Returns:
37
+ dict: Status and instance or error message.
38
+ """
39
+ try:
40
+ instance = DynamicsCore(*args, **kwargs)
41
+ return {"status": "success", "instance": instance}
42
+ except Exception as e:
43
+ return {"status": "error", "message": f"Failed to create DynamicsCore instance: {e}"}
44
+
45
+ # Potential Module
46
+ # -------------------------------------------------------------------------
47
+ def create_potential_core_instance(self, *args, **kwargs):
48
+ """
49
+ Create an instance of PotentialCore.
50
+
51
+ Parameters:
52
+ *args, **kwargs: Arguments for PotentialCore initialization.
53
+
54
+ Returns:
55
+ dict: Status and instance or error message.
56
+ """
57
+ try:
58
+ instance = PotentialCore(*args, **kwargs)
59
+ return {"status": "success", "instance": instance}
60
+ except Exception as e:
61
+ return {"status": "error", "message": f"Failed to create PotentialCore instance: {e}"}
62
+
63
+ def create_builtin_core_instance(self, *args, **kwargs):
64
+ """
65
+ Create an instance of BuiltinCore.
66
+
67
+ Parameters:
68
+ *args, **kwargs: Arguments for BuiltinCore initialization.
69
+
70
+ Returns:
71
+ dict: Status and instance or error message.
72
+ """
73
+ try:
74
+ instance = BuiltinCore(*args, **kwargs)
75
+ return {"status": "success", "instance": instance}
76
+ except Exception as e:
77
+ return {"status": "error", "message": f"Failed to create BuiltinCore instance: {e}"}
78
+
79
+ # Integration Module
80
+ # -------------------------------------------------------------------------
81
+ def create_integrate_core_instance(self, *args, **kwargs):
82
+ """
83
+ Create an instance of IntegrateCore.
84
+
85
+ Parameters:
86
+ *args, **kwargs: Arguments for IntegrateCore initialization.
87
+
88
+ Returns:
89
+ dict: Status and instance or error message.
90
+ """
91
+ try:
92
+ instance = IntegrateCore(*args, **kwargs)
93
+ return {"status": "success", "instance": instance}
94
+ except Exception as e:
95
+ return {"status": "error", "message": f"Failed to create IntegrateCore instance: {e}"}
96
+
97
+ # Coordinates Module
98
+ # -------------------------------------------------------------------------
99
+ def create_great_circle_instance(self, *args, **kwargs):
100
+ """
101
+ Create an instance of GreatCircle.
102
+
103
+ Parameters:
104
+ *args, **kwargs: Arguments for GreatCircle initialization.
105
+
106
+ Returns:
107
+ dict: Status and instance or error message.
108
+ """
109
+ try:
110
+ instance = GreatCircle(*args, **kwargs)
111
+ return {"status": "success", "instance": instance}
112
+ except Exception as e:
113
+ return {"status": "error", "message": f"Failed to create GreatCircle instance: {e}"}
114
+
115
+ # Orbit Module
116
+ # -------------------------------------------------------------------------
117
+ def create_orbit_instance(self, *args, **kwargs):
118
+ """
119
+ Create an instance of Orbit.
120
+
121
+ Parameters:
122
+ *args, **kwargs: Arguments for Orbit initialization.
123
+
124
+ Returns:
125
+ dict: Status and instance or error message.
126
+ """
127
+ try:
128
+ instance = Orbit(*args, **kwargs)
129
+ return {"status": "success", "instance": instance}
130
+ except Exception as e:
131
+ return {"status": "error", "message": f"Failed to create Orbit instance: {e}"}
132
+
133
+ # Error Handling
134
+ # -------------------------------------------------------------------------
135
+ def handle_import_failure(self):
136
+ """
137
+ Handle import failure gracefully.
138
+
139
+ Returns:
140
+ dict: Status and error message.
141
+ """
142
+ return {"status": "error", "message": "Import failed. Ensure the source directory is correctly set."}
gala/mcp_output/mcp_plugin/main.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Service Auto-Wrapper - Auto-generated
3
+ """
4
+ from mcp_service import create_app
5
+
6
+ def main():
7
+ """Main entry point"""
8
+ app = create_app()
9
+ return app
10
+
11
+ if __name__ == "__main__":
12
+ app = main()
13
+ app.run()
gala/mcp_output/mcp_plugin/mcp_service.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Add the local source directory to sys.path
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ if source_path not in sys.path:
7
+ sys.path.insert(0, source_path)
8
+
9
+ from fastmcp import FastMCP
10
+ from gala.dynamics.core import DynamicsCore
11
+ from gala.potential.potential.core import PotentialCore
12
+
13
+ mcp = FastMCP("gala_service")
14
+
15
+ @mcp.tool(name="calculate_dynamics", description="Calculate dynamics using the DynamicsCore module")
16
+ def calculate_dynamics(param1: float, param2: float) -> dict:
17
+ """
18
+ Calculate dynamics based on provided parameters.
19
+
20
+ :param param1: First parameter for dynamics calculation
21
+ :param param2: Second parameter for dynamics calculation
22
+ :return: Dictionary containing success, result, or error
23
+ """
24
+ try:
25
+ result = DynamicsCore.some_function(param1, param2)
26
+ return {"success": True, "result": result, "error": None}
27
+ except Exception as e:
28
+ return {"success": False, "result": None, "error": str(e)}
29
+
30
+ @mcp.tool(name="compute_potential", description="Compute potential using the PotentialCore module")
31
+ def compute_potential(param1: float, param2: float) -> dict:
32
+ """
33
+ Compute potential based on provided parameters.
34
+
35
+ :param param1: First parameter for potential computation
36
+ :param param2: Second parameter for potential computation
37
+ :return: Dictionary containing success, result, or error
38
+ """
39
+ try:
40
+ result = PotentialCore.some_function(param1, param2)
41
+ return {"success": True, "result": result, "error": None}
42
+ except Exception as e:
43
+ return {"success": False, "result": None, "error": str(e)}
44
+
45
+ def create_app() -> FastMCP:
46
+ """
47
+ Create and return the FastMCP application instance.
48
+
49
+ :return: FastMCP instance
50
+ """
51
+ return mcp
gala/mcp_output/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastmcp
2
+ fastapi
3
+ uvicorn[standard]
4
+ pydantic>=2.0.0
5
+ numpy>=1.26.4
6
+ scipy>=1.12,<1.17
7
+ astropy>=6.0
8
+ pyyaml
9
+ cython>=0.29
gala/mcp_output/start_mcp.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ MCP Service Startup Entry
4
+ """
5
+ import sys
6
+ import os
7
+
8
+ project_root = os.path.dirname(os.path.abspath(__file__))
9
+ mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
+ if mcp_plugin_dir not in sys.path:
11
+ sys.path.insert(0, mcp_plugin_dir)
12
+
13
+ from mcp_service import create_app
14
+
15
+ def main():
16
+ """Start FastMCP service"""
17
+ app = create_app()
18
+ # Use environment variable to configure port, default 8000
19
+ port = int(os.environ.get("MCP_PORT", "8000"))
20
+
21
+ # Choose transport mode based on environment variable
22
+ transport = os.environ.get("MCP_TRANSPORT", "stdio")
23
+ if transport == "http":
24
+ app.run(transport="http", host="0.0.0.0", port=port)
25
+ else:
26
+ # Default to STDIO mode
27
+ app.run()
28
+
29
+ if __name__ == "__main__":
30
+ main()
gala/mcp_output/workflow_summary.json ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repository": {
3
+ "name": "gala",
4
+ "url": "https://github.com/adrn/gala",
5
+ "local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/gala",
6
+ "description": "Python library",
7
+ "features": "Basic functionality",
8
+ "tech_stack": "Python",
9
+ "stars": 0,
10
+ "forks": 0,
11
+ "language": "Python",
12
+ "last_updated": "",
13
+ "complexity": "medium",
14
+ "intrusiveness_risk": "medium"
15
+ },
16
+ "execution": {
17
+ "start_time": 1770208770.296307,
18
+ "end_time": 1770208855.1712303,
19
+ "duration": 84.87492346763611,
20
+ "status": "success",
21
+ "workflow_status": "success",
22
+ "nodes_executed": [
23
+ "download",
24
+ "analysis",
25
+ "env",
26
+ "generate",
27
+ "run",
28
+ "review",
29
+ "finalize"
30
+ ],
31
+ "total_files_processed": 2,
32
+ "environment_type": "unknown",
33
+ "llm_calls": 0,
34
+ "deepwiki_calls": 0
35
+ },
36
+ "tests": {
37
+ "original_project": {
38
+ "passed": false,
39
+ "details": {},
40
+ "test_coverage": "100%",
41
+ "execution_time": 0,
42
+ "test_files": []
43
+ },
44
+ "mcp_plugin": {
45
+ "passed": true,
46
+ "details": {},
47
+ "service_health": "healthy",
48
+ "startup_time": 0,
49
+ "transport_mode": "stdio",
50
+ "fastmcp_version": "unknown",
51
+ "mcp_version": "unknown"
52
+ }
53
+ },
54
+ "analysis": {
55
+ "structure": {
56
+ "packages": [
57
+ "source.src.gala",
58
+ "source.tests.integrate"
59
+ ]
60
+ },
61
+ "dependencies": {
62
+ "has_environment_yml": false,
63
+ "has_requirements_txt": false,
64
+ "pyproject": true,
65
+ "setup_cfg": false,
66
+ "setup_py": true
67
+ },
68
+ "entry_points": {
69
+ "imports": [],
70
+ "cli": [],
71
+ "modules": []
72
+ },
73
+ "risk_assessment": {
74
+ "import_feasibility": 0.8,
75
+ "intrusiveness_risk": "medium",
76
+ "complexity": "medium"
77
+ },
78
+ "deepwiki_analysis": {
79
+ "repo_url": "https://github.com/adrn/gala",
80
+ "repo_name": "gala",
81
+ "content": "Galactic and gravitational dynamics in Python\nRepository Not Indexed\nThis repository hasn't been indexed yet. Indexing allows you to explore code structure, find documentation, and understand dependencies.\nIndexing typically takes 2-10 minutes to complete after it starts indexing\nOnce indexed, you'll have full access to code exploration and search functionality",
82
+ "model": "gpt-4o-2024-08-06",
83
+ "source": "selenium",
84
+ "success": true
85
+ },
86
+ "code_complexity": {
87
+ "cyclomatic_complexity": "medium",
88
+ "cognitive_complexity": "medium",
89
+ "maintainability_index": 75
90
+ },
91
+ "security_analysis": {
92
+ "vulnerabilities_found": 0,
93
+ "security_score": 85,
94
+ "recommendations": []
95
+ }
96
+ },
97
+ "plugin_generation": {
98
+ "files_created": [
99
+ "mcp_output/start_mcp.py",
100
+ "mcp_output/mcp_plugin/__init__.py",
101
+ "mcp_output/mcp_plugin/mcp_service.py",
102
+ "mcp_output/mcp_plugin/adapter.py",
103
+ "mcp_output/mcp_plugin/main.py",
104
+ "mcp_output/requirements.txt",
105
+ "mcp_output/README_MCP.md"
106
+ ],
107
+ "main_entry": "start_mcp.py",
108
+ "requirements": [
109
+ "fastmcp>=0.1.0",
110
+ "pydantic>=2.0.0"
111
+ ],
112
+ "readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/gala/mcp_output/README_MCP.md",
113
+ "adapter_mode": "import",
114
+ "total_lines_of_code": 0,
115
+ "generated_files_size": 0,
116
+ "tool_endpoints": 0,
117
+ "supported_features": [
118
+ "Basic functionality"
119
+ ],
120
+ "generated_tools": [
121
+ "Basic tools",
122
+ "Health check tools",
123
+ "Version info tools"
124
+ ]
125
+ },
126
+ "code_review": {},
127
+ "errors": [],
128
+ "warnings": [],
129
+ "recommendations": [
130
+ "Improve test coverage by adding more unit tests",
131
+ "Ensure all existing tests are passing and fix any failing tests",
132
+ "Consider adding integration tests to cover interactions between modules",
133
+ "Optimize large files for better maintainability",
134
+ "such as 'src/gala/dynamics/core.py' and 'src/gala/potential/potential/builtin/core.py'",
135
+ "Document the codebase more thoroughly",
136
+ "especially for complex modules",
137
+ "Ensure the repository is indexed for better code exploration and search functionality",
138
+ "Update the 'requirements.txt' and 'environment.yml' files to manage dependencies more effectively",
139
+ "Consider using 'setup.cfg' for configuration to simplify the setup process",
140
+ "Review and optimize the import strategy to reduce complexity and risk",
141
+ "Enhance the README file to provide clearer instructions for setup and usage",
142
+ "Regularly update dependencies to the latest versions to ensure compatibility and security",
143
+ "Conduct a code review to identify potential improvements in code quality and performance",
144
+ "Implement performance metrics to monitor and improve the efficiency of the codebase",
145
+ "Explore opportunities to reduce the intrusiveness risk and complexity of the project."
146
+ ],
147
+ "performance_metrics": {
148
+ "memory_usage_mb": 0,
149
+ "cpu_usage_percent": 0,
150
+ "response_time_ms": 0,
151
+ "throughput_requests_per_second": 0
152
+ },
153
+ "deployment_info": {
154
+ "supported_platforms": [
155
+ "Linux",
156
+ "Windows",
157
+ "macOS"
158
+ ],
159
+ "python_versions": [
160
+ "3.8",
161
+ "3.9",
162
+ "3.10",
163
+ "3.11",
164
+ "3.12"
165
+ ],
166
+ "deployment_methods": [
167
+ "Docker",
168
+ "pip",
169
+ "conda"
170
+ ],
171
+ "monitoring_support": true,
172
+ "logging_configuration": "structured"
173
+ },
174
+ "execution_analysis": {
175
+ "success_factors": [
176
+ "Successful execution of all workflow nodes",
177
+ "Healthy service status of MCP plugin"
178
+ ],
179
+ "failure_reasons": [],
180
+ "overall_assessment": "good",
181
+ "node_performance": {
182
+ "download_time": "Completed successfully, indicating efficient data retrieval",
183
+ "analysis_time": "Completed successfully, indicating effective code analysis",
184
+ "generation_time": "Completed successfully, indicating efficient code generation",
185
+ "test_time": "Original project tests failed, indicating potential issues with test setup or code"
186
+ },
187
+ "resource_usage": {
188
+ "memory_efficiency": "Memory usage data not provided, unable to assess",
189
+ "cpu_efficiency": "CPU usage data not provided, unable to assess",
190
+ "disk_usage": "Disk usage data not provided, unable to assess"
191
+ }
192
+ },
193
+ "technical_quality": {
194
+ "code_quality_score": 75,
195
+ "architecture_score": 70,
196
+ "performance_score": 65,
197
+ "maintainability_score": 75,
198
+ "security_score": 85,
199
+ "scalability_score": 70
200
+ }
201
+ }
gala/source/.clang-format ADDED
@@ -0,0 +1 @@
 
 
1
+ ColumnLimit: 88
gala/source/.pre-commit-config.yaml ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ci:
2
+ autoupdate_commit_msg: "chore: update pre-commit hooks"
3
+ autofix_commit_msg: "style: pre-commit fixes"
4
+
5
+ repos:
6
+ - repo: https://github.com/pre-commit/pre-commit-hooks
7
+ rev: "v6.0.0"
8
+ hooks:
9
+ - id: check-case-conflict
10
+ - id: check-merge-conflict
11
+ - id: check-symlinks
12
+ # - id: check-yaml
13
+ - id: debug-statements
14
+ - id: end-of-file-fixer
15
+ - id: mixed-line-ending
16
+ - id: trailing-whitespace
17
+
18
+ - repo: https://github.com/pre-commit/pygrep-hooks
19
+ rev: "v1.10.0"
20
+ hooks:
21
+ - id: rst-directive-colons
22
+ - id: rst-inline-touching-normal
23
+
24
+ - repo: https://github.com/pre-commit/mirrors-prettier
25
+ rev: "v4.0.0-alpha.8"
26
+ hooks:
27
+ - id: prettier
28
+ types_or: [markdown, html, css, scss, javascript, json]
29
+ args: [--prose-wrap=always]
30
+
31
+ - repo: https://github.com/astral-sh/ruff-pre-commit
32
+ rev: "v0.14.14"
33
+ hooks:
34
+ - id: ruff-check
35
+ types_or: [python, pyi, jupyter]
36
+ args: ["--fix", "--show-fixes", "--unsafe-fixes"]
37
+ - id: ruff-format
38
+ types_or: [python, pyi, jupyter]
39
+
40
+ - repo: https://github.com/adamchainz/blacken-docs
41
+ rev: "1.20.0"
42
+ hooks:
43
+ - id: blacken-docs
44
+ additional_dependencies: [black==23.*]
45
+
46
+ - repo: https://github.com/shellcheck-py/shellcheck-py
47
+ rev: "v0.11.0.1"
48
+ hooks:
49
+ - id: shellcheck
50
+
51
+ - repo: https://github.com/abravalheri/validate-pyproject
52
+ rev: "v0.24.1"
53
+ hooks:
54
+ - id: validate-pyproject
55
+ additional_dependencies:
56
+ - validate-pyproject[all]
57
+ - validate-pyproject-schema-store[all]
58
+ - ruff>=0.13
59
+
60
+ - repo: https://github.com/python-jsonschema/check-jsonschema
61
+ rev: "0.36.1"
62
+ hooks:
63
+ - id: check-dependabot
64
+ - id: check-github-workflows
65
+ - id: check-readthedocs
gala/source/.prettierrc.toml ADDED
@@ -0,0 +1 @@
 
 
1
+ printWidth = 88
gala/source/.readthedocs.yml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-22.04
5
+ tools:
6
+ python: "3.12"
7
+ jobs:
8
+ post_checkout:
9
+ # Use `git log` to check if the latest commit contains "skip docs",
10
+ # in that case exit the command with 183 to cancel the build
11
+ - (git --no-pager log --pretty="tformat:%s -- %b" -1 | grep -viq "skip docs") ||
12
+ exit 183
13
+ apt_packages:
14
+ - gsl-bin
15
+ - libgsl-dev
16
+ - graphviz
17
+
18
+ python:
19
+ install:
20
+ - method: pip
21
+ path: .
22
+ extra_requirements:
23
+ - docs
24
+
25
+ sphinx:
26
+ configuration: docs/conf.py
27
+
28
+ formats: []
gala/source/AUTHORS.rst ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **Main author:** Adrian Price-Whelan (`@adrn <https://github.com/adrn>`_)
2
+
3
+ All contributors (alphabetical last name):
4
+
5
+ * Bill Chen (@ybillchen)
6
+ * Dan Foreman-Mackey (@dfm)
7
+ * Nico Garavito-Camargo (@jngaravitoc)
8
+ * Lehman Garrison (@lgarrison)
9
+ * Johnny Greco (@johnnygreco)
10
+ * Akeem Hart (@akeemlh)
11
+ * Sergey Koposov (@segasai)
12
+ * Alex Kurek (@AlexKurek)
13
+ * Daniel Lenz (@DanielLenz)
14
+ * Zhaozhou Li (@syrte)
15
+ * Sophia Lilleengen (@sophialilleengen)
16
+ * Pey Lian Lim (@pllim)
17
+ * Semyeong Oh (@smoh)
18
+ * Clément Robert (@neutrinoceros)
19
+ * Brigitta Sipőcz (@bsipocz)
20
+ * Harrison Souchereau (@HSouch)
21
+ * Nathaniel Starkman (@nstarman)
22
+ * Tom Wagg (@tomwagg)
gala/source/CHANGES.rst ADDED
@@ -0,0 +1,725 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 1.12.0 (unreleased)
2
+ ===================
3
+
4
+ New Features
5
+ ------------
6
+
7
+ Bug fixes
8
+ ---------
9
+
10
+ - Fixed a bug in ``TimeInterpolatedPotential`` that caused errors when trying to pickle
11
+ the object (or use it within a multiprocessing or MPI pool).
12
+
13
+ API changes
14
+ -----------
15
+
16
+ Other
17
+ -----
18
+
19
+
20
+ 1.11.0 (2025-12-10)
21
+ ===================
22
+
23
+ New Features
24
+ ------------
25
+
26
+ - A C++ compiler is now required to build Gala from source.
27
+ - Orbit integration performance is improved by changing the way vectorization over
28
+ multiple orbits is handled internally.
29
+ - Added a new ``gala.potential.SphericalSplinePotential`` class for representing
30
+ generic spherical potentials with spline interpolation in either potential, density,
31
+ or mass enclosed.
32
+ - Added coordinate symmetry support for potential classes: spherical potential methods
33
+ can now be evaluated using ``r=`` and cylindrical potentials using ``R=`` and ``z=``
34
+ instead of requiring full 3D Cartesian coordinates.
35
+ - Added a new ``gala.potential.TimeInterpolatedPotential`` class that enables wrapping
36
+ any potential class to support time-dependent parameters through interpolation.
37
+ - Unit systems in potential classes can now be specified using string names
38
+ (e.g., ``'galactic'``, ``'dimensionless'``, etc.) when initializing potential
39
+ instances or when replacing units with ``.replace_units()``.
40
+ - C-level integrator arguments can now be specified when running a mock stream
41
+ simulation through the ``Integrator_kwargs`` in ``MockStreamGenerator.run()``.
42
+ - Added support for using the Leapfrog integrator with ``MockStreamGenerator`` (pass
43
+ ``Integrator=gi.LeapfrogIntegrator`` in ``MockStreamGenerator.run()``).
44
+ - Integrators can now be specified using lowercase string names (e.g., 'leapfrog',
45
+ 'dopri853', 'ruth4') in ``Hamiltonian.integrate_orbit()``,
46
+ ``DirectNBody.integrate_orbit()``, and ``MockStreamGenerator.run()``.
47
+ - Added a new method ``MockStream.rotate_to_progenitor_plane()`` that transforms a mock
48
+ stream into a new coordinate system where the progenitor's orbital plane is aligned
49
+ with the xy-plane, the stream and progenitor are centered at (0, 0), and the stream
50
+ primarily extends in the x direction (leading tail at positive x and trailing tail at
51
+ negative x).
52
+ - EXP: constructing potentials from pyEXP objects is now supported via
53
+ ``gala.potential.PyEXPPotential``.
54
+ - EXP: force evaluation with ``gala.potential.EXPPotential`` should now be much faster.
55
+
56
+ Bug fixes
57
+ ---------
58
+
59
+ - ``gala.potential.EXPPotential`` now propagates C++ exceptions to Python.
60
+ - Fixed a deprecation warning with astropy>=v7.1.
61
+ - Fixed a matplotlib warning when plotting an orbit with ``plot()`` and
62
+ ``autolim=True`` related to using ``aspect="equal"``.
63
+ - ``replicate()`` and ``replace_units`` now work with ``EXPPotential``.
64
+ - Fixed incorrect results from ``MockStreamGenerator.run()`` with an
65
+ ``n_particles`` array.
66
+ - Fixed a bug in ``MockStreamGenerator.run()`` when using ``DirectNBody`` with
67
+ ``output_filename`` and more bodies than stream particles, which caused a "TypeError:
68
+ Can't broadcast" error.
69
+ - Fixed a bug that caused ``GreatCircleICRSFrame`` to throw an error about a missing
70
+ attribute ``_R`` when transforming to/from the frame.
71
+
72
+ API changes
73
+ -----------
74
+
75
+ - Added ``copy`` kwarg to ``gala.dynamics.PhaseSpacePosition``,
76
+ ``gala.dynamics.Orbit``, and ``gala.dynamics.MockStream``.
77
+ - The `integrate_orbit()` method now validates that the input initial conditions have
78
+ the correct shape given the dimensionality of the potential.
79
+ - Removed custom ``ImmutableDict`` implementation in favor of
80
+ ``types.MappingProxyType``.
81
+ - Added new keyword arguments ``ndim`` and ``convert`` to ``PotentialParameter`` to
82
+ control the expected number of dimensions for array parameters and to specify a
83
+ conversion function for parameter values, respectively.
84
+ - Removed deprecated ``gala.dynamics.find_actions`` in favor of ``find_actions_o2gf``.
85
+ - Removed deprecated ``radial=True`` kwarg from ``Orbit.estimate_period()``.
86
+ - Removed deprecated ``value()`` method from potential clases in favor of using the
87
+ ``__call__()`` method or ``energy()``.
88
+ - Removed deprecated ``to_galpy_potential()`` method from potential classes in favor of
89
+ using ``as_interop("galpy")``.
90
+ - The Gala Milky Way potential classes (``MilkyWayPotential`` and
91
+ ``MilkyWayPotential2022``) have been combined into a single ``MilkyWayPotential``
92
+ class with a ``version=`` kwarg to specify the desired version (e.g., ``'v1'``,
93
+ ``'v2'``, etc.). The old classes are deprecated and will be removed in a future
94
+ release.
95
+
96
+ Build changes
97
+ -------------
98
+ - EXP: the instructions to build Gala against EXP have changed. Only the EXP install
99
+ dir is now used.
100
+
101
+ Other
102
+ -----
103
+
104
+ - Refactored package layout to move all source code into a ``src/`` directory, and move
105
+ all tests into a top-level ``tests/`` directory.
106
+
107
+
108
+ 1.10.1 (2025-08-21)
109
+ ===================
110
+
111
+ Bug fixes
112
+ ---------
113
+
114
+ - Support for ``gala.potential.EXPPotential`` in composite potentials is fixed.
115
+ - File path handling in ``gala.potential.EXPPotential`` is improved.
116
+
117
+
118
+ 1.10.0 (2025-07-31)
119
+ ===================
120
+
121
+ New Features
122
+ ------------
123
+
124
+ - Added a new ``SimulationUnitSystem`` class for handling unit systems in
125
+ simulations, especially for N-body simulations.
126
+
127
+ - Added options ``error_if_fail`` and ``log_output`` to integrator kwargs for the
128
+ dop853 integrator, along with some other arguments that are passed directly to the C
129
+ integrator (e.g., ``nstiff``). ``error_if_fail`` controls whether Python will raise
130
+ an error if the C integrator fails to integrate an orbit, and ``log_output`` will log
131
+ the output of the integrator (primarily for errors) to stdout. See the docstring for `
132
+ ``gala.integrate.DOP853Integrator`` for more information about all of the available
133
+ options for the integrator.
134
+
135
+ - You may now specify a ``gala.units.UnitSystem`` instance to control the units of
136
+ plotted components when using ``gala.dynamics.Orbit.plot()`` or
137
+ ``gala.dynamics.PhaseSpacePosition.plot()``.
138
+
139
+ - Added the ability to specify integer or string (i.e. non-Quantity) potential
140
+ parameters.
141
+
142
+ - Added ``gala.potential.EXPPotential`` for using basis function expansion potentials
143
+ from EXP.
144
+
145
+ - Added methods ``NFWPotential.M200()``, ``NFWPotential.R200()``,
146
+ ``NFWPotential.c200()`` to compute the characteristic mass, radius, and concentration
147
+ of an NFW instance.
148
+
149
+ Bug fixes
150
+ ---------
151
+
152
+ - Fixed a longstanding issue with orbit integration where there was a maximum number of
153
+ orbits that could be integrated simultaneously. Now, arrays are allocated dynamically
154
+ and there is no limit.
155
+
156
+ - Similarly, fixed a longstanding issue that restricted the number of potential
157
+ components that could be added to a composite potential. Now, arrays are allocated
158
+ dynamically and there is no limit.
159
+
160
+ - Some versions of Agama do not accept astropy.units objects as input to setUnits. Gala
161
+ now converts to floats to set the unit scales in agama when converting a potential to
162
+ Agama (using ``potential.as_interop("agama")``).
163
+
164
+ - Fixed a bug in ``MockStreamGenerator.run()`` where passing an array of length 1 for
165
+ the progenitor mass would lead to a silent failure of the stream generation.
166
+
167
+ - Fixed the normalization of the ``PowerLawCutoffPotential`` potential energy so that it
168
+ goes to zero at infinity.
169
+
170
+ API changes
171
+ -----------
172
+
173
+ - Gala has ``save_all`` and ``store_all`` flags for saving all orbits at every
174
+ timestep. The ``store_all`` flag is now deprecated and will be removed in a future
175
+ release. The ``save_all`` flag should be used instead.
176
+
177
+ Other
178
+ -----
179
+
180
+ - Added a flag to skip rotating and/or shifting input coordinates when computing
181
+ potential, density, gradient, and hessian values. This leads to some free performance
182
+ improvements in existing code!
183
+
184
+ - Refactored the way integration is done with the DOP853 integrator. The integrator now
185
+ uses the dense output feature (which uses interpolation) to compute the output values
186
+ at the requested times. This is a significant performance improvement for large
187
+ numbers of orbits, and also allows for much faster results when integrating over long
188
+ timescales.
189
+
190
+ 1.9.1 (2024-08-26)
191
+ ==================
192
+
193
+ - This release fixes the wheel builds for linux and mac and no new features or bug fixes
194
+ are included.
195
+
196
+
197
+ 1.9.0 (2024-08-22)
198
+ ==================
199
+
200
+ New Features
201
+ ------------
202
+
203
+ - Added an option to specify a multiprocessing or parallel processing pool when
204
+ computing basis function coefficients for the SCF potential from discrete particles.
205
+
206
+ - Added the Burkert potential as a built-in cpotential.
207
+
208
+ - Added a method to generate the Burkert potential with just r0 as an input
209
+
210
+ - Added new particle spray method by Chen et al. (2024).
211
+
212
+ Bug fixes
213
+ ---------
214
+
215
+ - Fixed the parameter values in the ``FardalStreamDF`` class to be consistent with
216
+ the values used in Fardal et al. (2015). Added an option ``gala_modified`` to the
217
+ class to enable using the new (correct) parameter values, but the default will
218
+ continue to use the Gala modified values (for backwards compatibility).
219
+
220
+ - Improved internal efficiency of ``DirectNBody``.
221
+
222
+ - Fixed a bug in which passing a ``DirectNBody`` instance to the ``MockStreamGenerator.
223
+ run()`` would fail if ``save_all=False`` in the nbody instance.
224
+
225
+ - Fixed an incompatibility with Astropy v6.1 and above where ``_make_getter`` was
226
+ removed.
227
+
228
+
229
+ API changes
230
+ -----------
231
+
232
+ - Deprecated ``gala.integrate.Integrator.run`` for
233
+ ``gala.integrate.Integrator.__call__``. The old method will raise a warning
234
+ and will be removed in a future release.
235
+
236
+
237
+ 1.8.1 (2023-12-31)
238
+ ==================
239
+
240
+ - New release to fix upload to PyPI from GitHub Actions and invalid pin in pyia
241
+ dependency.
242
+
243
+
244
+ 1.8 (2023-12-23)
245
+ ================
246
+
247
+ New Features
248
+ ------------
249
+
250
+ - Added a ``.guiding_center()`` method to ``PhaseSpacePosition`` and ``Orbit`` to
251
+ compute the guiding center radius.
252
+
253
+ - Added a way to convert Gala potential instances to Agama potential instances.
254
+
255
+ Bug fixes
256
+ ---------
257
+
258
+ - Fixed a bug with the ``plot_contours()`` and ``plot_density_contours()`` methods so
259
+ that times specified are now passed through correctly to the potential methods.
260
+
261
+ - Fixed the YAML output to use ``default_flow_style=None`` for serializing potential
262
+ objects, which leads to a more efficient array output.
263
+
264
+ - ``scf.compute_coeffs_discrete`` now raises an error if GSL is not enabled rather than
265
+ silently returning zeros
266
+
267
+ - ``SCFPotential`` will now work with IO functions (``save`` & ``load``)
268
+
269
+ - Fixes compatibility with Astropy v6.0
270
+
271
+ API changes
272
+ -----------
273
+
274
+ - Changed the way potential interoperability is done with other Galactic dynamics
275
+ packages (Agama, galpy, etc.). It is now handled by the ``Potential.as_interop()``
276
+ method on all potential class instances.
277
+
278
+
279
+ 1.7.1 (2023-08-05)
280
+ ==================
281
+
282
+ - Switched build system to use pyproject.toml instead of setup.cfg
283
+
284
+ 1.7 (2023-08-05)
285
+ ================
286
+
287
+ New Features
288
+ ------------
289
+
290
+ - Added a method to export the internal components of an
291
+ ``MN3ExponentialDiskPotential()`` to three ``MiyamotoNagaiPotential`` instances.
292
+
293
+ - Added a new Milky Way potential model: ``MilkyWayPotential2022``, which is based on
294
+ updated measurements of the disk structure and circular velocity curve of the disk.
295
+
296
+ - Added the ability to use leapfrog integration within the ``DirectNBody`` integrator.
297
+
298
+ - Added a new coordinate frame for the Vasiliev+2021 Sagittarius stream coordinate
299
+ system, ``SagittariusVasiliev21``.
300
+
301
+ Bug fixes
302
+ ---------
303
+
304
+ - Fixed a bug with the ``OrphanKoposov19()`` coordinate frame that caused the wrong
305
+ rotation matrix to be returned.
306
+
307
+ - Fixed an ``AstropyDeprecationWarning`` resulting from the use of ``override__dir__``.
308
+
309
+ - Fixed a bug in ``Orbit.estimate_period()`` that would cause the method to fail with a
310
+ ``UnitsError`` if one orbit returned a nan value for the period.
311
+
312
+ - Fixed a bug when compiling the ``dop853`` integrator.
313
+
314
+ API changes
315
+ -----------
316
+
317
+ - Refactored the way ``GreatCircleICRSFrame()`` works to be more consistent and
318
+ unambiguous with coordinate frame definitions. The frame now requires an input pole
319
+ and origin, but can be initialized in old ways using the ``from_*()`` class methods
320
+ (e.g., with ``pole`` and ``ra0`` values).
321
+
322
+
323
+ 1.6.1 (2022-11-07)
324
+ ==================
325
+
326
+ Bug fixes
327
+ ---------
328
+
329
+ - Properly incorporate commits related to ``SCFInterpolatedPotential``.
330
+
331
+
332
+ 1.6 (2022-11-07)
333
+ ================
334
+
335
+ New Features
336
+ ------------
337
+
338
+ - Added a ``.replicate()`` method to Potential classes to enable copying
339
+ potential objects but modifying some parameter values.
340
+
341
+ - Added a new potential class ``MN3ExponentialDiskPotential`` based on Smith et
342
+ al. (2015): an approximation of the potential generated by a double
343
+ exponential disk using a sum of three Miyamoto-Nagai disks.
344
+
345
+ - The ``Orbit.estimate_period()`` method now returns period estimates in all
346
+ phase-space components instead of just the radial period.
347
+
348
+ - Added a ``store_all`` flag to the integrators to control whether to save
349
+ phase-space information for all timesteps or only the final timestep.
350
+
351
+ - Added a ``plot_rotation_curve()`` method to all potential objects to make a 1D plot
352
+ of the circular velocity curve.
353
+
354
+ - Added a new potential for representing multipole expansions ``MultipolePotential``.
355
+
356
+ - Added a new potential ``CylSplinePotential`` for flexible representation of
357
+ axisymmetric potentials by allowing passing in grids of potential values
358
+ evaluated grids of R, z values (like the ``CylSpline`` potential in Agama).
359
+
360
+ - Added a ``show_time`` flag to ``Orbit.animate()`` to control whether to show the
361
+ current timestep.
362
+
363
+ - Changed ``Orbit.animate()`` to allow for different ``marker_style`` and
364
+ ``segment_style`` options for individual orbits by passing a list of dicts instead
365
+ of just a dict.
366
+
367
+ - Added an experimental new class ``SCFInterpolatedPotential`` that accepts a time
368
+ series of coefficients and interpolates the coefficient values to any evaluation time.
369
+
370
+ Bug fixes
371
+ ---------
372
+
373
+ - Fixed a bug where the ``NFWPotential`` energy was nan when evaluating at the
374
+ origin, and added tests for all potentials to check for a finite value of the
375
+ potential at the origin (when expected).
376
+
377
+ - Fixed a bug in ``NFWPotential.from_M200_c()`` where the incorrect scale radius
378
+ was computed (Cython does not always use Python 3 division rules for dividing
379
+ integers!).
380
+
381
+ - Fixed a bug in the (C-level/internal) estimation of the 2nd derivative of the
382
+ potential, used to generate mock streams, that affects non-conservative force
383
+ fields.
384
+
385
+ API changes
386
+ -----------
387
+
388
+ - The ``Orbit.estimate_period()`` method now returns period estimates in all
389
+ phase-space components instead of just the radial period.
390
+
391
+
392
+ 1.5 (2022-03-03)
393
+ ================
394
+
395
+ New Features
396
+ ------------
397
+
398
+ - Implemented a basic progress bar for integrating orbits and mock streams. Pass
399
+ ``progress=True`` with ``Integrator_kwargs`` when calling
400
+ ``.integrate_orbit()``, or pass ``progress=True`` to
401
+ ``MockStreamGenerator.run()``.
402
+
403
+ - Added a new symplectic integrator: The Ruth 4th-order integrator, implemented
404
+ with the class ``Ruth4Integrator``.
405
+
406
+ - Added a ``Orbit.animate()`` method to make ``matplotlib`` animations of
407
+ orbits.
408
+
409
+ - Modified ``Orbit._max_helper()`` to use a parabola instead of interpolation
410
+
411
+ - Added functionality to transform from action-angle coordinates to Cartesian
412
+ position velocity coordinates in the Isochrone potential:
413
+ ``gala.dynamics.actionangle.isochrone_aa_to_xv()``.
414
+
415
+ - Added a new method on ``DirectNBody`` to enable computing the instantaneous,
416
+ mutual, N-body acceleration vectors ``DirectNBody.acceleration()``.
417
+
418
+ Bug fixes
419
+ ---------
420
+
421
+ - Fixed ``find_actions()`` to accept an ``Orbit`` instance with multiple orbits.
422
+
423
+ - Fixed a bug that appeared when trying to release all mock stream particles at
424
+ the same timestep (e.g., pericenter).
425
+
426
+ - Fixed a bug where time arrays returned from ``parse_time_specification``
427
+ could come back with a non-float64 dtype.
428
+
429
+ - Fixed a bug with ``DirectNBody`` with composite potentials where only the
430
+ first potential component would move as a body / particle.
431
+
432
+ - Fixed a bug with the Python implementation of Leapfrog integration
433
+ ``LeapfrogIntegrator`` that led to incorrect orbits for non-conservative
434
+ systems that were integrated backwards (i.e. with ``dt<<0``).
435
+
436
+ - Fixed a bug with the ``FlattenedNFW`` potential class in which the energy and
437
+ gradient functions were not using the inputted flattening (``c`` value) and
438
+ were instead defaulting to the spherical NFW model.
439
+
440
+ - Enabled pickling ``Frame`` instances and therefore now ``Hamiltonian``
441
+ instances.
442
+
443
+ - Fixed a bug with ``autolim=True`` during Orbit plotting where the axes limits
444
+ were only dependent on the most recent Orbit rather than all that were present
445
+ on the axis
446
+
447
+ API changes
448
+ -----------
449
+
450
+ - Renamed ``gala.dynamics.actionangle.isochrone_to_aa()`` to
451
+ ``gala.dynamics.actionangle.isochrone_xv_to_aa()``
452
+
453
+ - Renamed ``gala.dynamics.actionangle.find_actions()`` to
454
+ ``gala.dynamics.actionangle.find_actions_o2gf()``
455
+
456
+
457
+ 1.4.1 (2021-07-01)
458
+ ==================
459
+
460
+ - Fixed a RST bug that caused the README to fail to render.
461
+
462
+
463
+ 1.4 (2021-07-01)
464
+ ================
465
+
466
+ New Features
467
+ ------------
468
+
469
+ - ``UnitSystem`` objects can now be created with custom units passed in as
470
+ Astropy ``Quantity`` objects.
471
+
472
+ - Added functionality to convert Gala potential objects to Galpy potential
473
+ objects, or to create Gala potential objects from a pre-existing Galpy
474
+ potential.
475
+
476
+ - Added a ``plot_3d()`` method for ``Orbit`` objects to make 3D plots of the
477
+ orbital trajectories.
478
+
479
+ Bug fixes
480
+ ---------
481
+
482
+ - Fixed a bug when calling ``orbit.norbits`` when the representation is not
483
+ cartesian.
484
+
485
+ - Fixed a bug with ``GreatCircleICRSFrame.from_endpoints()`` that caused an
486
+ error when the input coordinates had associated velocity data.
487
+
488
+ - Fixed a bug with the ``JaffePotential`` density evaluation, which was too low
489
+ by a factor of two.
490
+
491
+ - Implemented a density function for ``LogarithmicPotential``, which was
492
+ missing previously.
493
+
494
+ - The analytic action-angle and ``find_actions()`` utilities now correctly
495
+ return frequencies with angular frequency units rather than frequency.
496
+
497
+ API changes
498
+ -----------
499
+
500
+ - Removed the deprecated ``gala.coordinates.get_galactocentric2019()`` function.
501
+
502
+
503
+ 1.3 (2020-10-27)
504
+ ================
505
+
506
+ New Features
507
+ ------------
508
+
509
+ - Added a new ``.to_sympy()`` classmethod for the ``Potential`` classes to
510
+ return a sympy expression and variables.
511
+
512
+ - Added a method, ``.to_galpy_orbit()``, to convert Gala ``Orbit`` instances to
513
+ Galpy ``Orbit`` objects.
514
+
515
+ - The ``NFWPotential`` can now be instantiated via a new classmethod:
516
+ ``NFWPotential.from_M200_c()``, which accepts a virial mass and a
517
+ concentration.
518
+
519
+ - Added a fast way of computing the Staeckel focal length, ``Delta``, using
520
+ Gala potential classes, ``gala.dynamics.get_staeckel_fudge_delta``
521
+
522
+ Bug fixes
523
+ ---------
524
+
525
+ - Fixed a bug with ``Potential`` classes ``.replace_units()`` so that classes
526
+ with dimensionless unit systems cannot be replaced with physical unit systems,
527
+ and vice versa.
528
+
529
+ - Implemented Hessian functions for most potentials.
530
+
531
+ - Fixed ``.to_latex()`` to properly return a latex representation of the
532
+ potential. This uses the new ``.to_sympy()`` method under the hood.
533
+
534
+ - Potential classes now validate that input positions have dimensionality that
535
+ matches what is expected for each potential.
536
+
537
+ API changes
538
+ -----------
539
+
540
+ - Changed the way new ``Potential`` classes are defined: they now rely on
541
+ defining class-level ``PotentialParameter`` objects, which reduces a
542
+ significant amount of boilerplate code in the built-in potentials.
543
+
544
+
545
+ 1.2 (2020-07-13)
546
+ ================
547
+
548
+ - Gala now builds on Windows!
549
+
550
+ New Features
551
+ ------------
552
+
553
+ - Added a coordinate frame for the Pal 13 stream, ``Pal13Shipp20``.
554
+
555
+ Bug fixes
556
+ ---------
557
+
558
+ - Fixed a bug with the mock stream machinery in which the stream would not
559
+ integrate for the specified number of timesteps if an array of
560
+ ``n_particles`` was passed in with 0's near the end of the array.
561
+
562
+
563
+ 1.1 (2020-03-08)
564
+ ================
565
+
566
+ New Features
567
+ ------------
568
+ - Potential objects now support replacing the unit system with the
569
+ ``.replace_units()`` method, or by updating the ``.units`` attribute on an
570
+ existing instance.
571
+ - Added a ``DirectNBody`` class that supports direct N-body orbit integration in
572
+ (optional) external background potentials.
573
+ - Added a coordinate frame for the Jhelum stream, ``JhelumBonaca19``.
574
+ - Added a function for retrieving a more modern Galactocentric reference frame,
575
+ ``gala.coordinates.get_galactocentric2019()``.
576
+ - Added a classmethod to allow initializing a ``GreatCircleICRSFrame`` from a
577
+ rotation matrix that specifies the transformation from ``ICRS`` to the great
578
+ circle frame.
579
+
580
+ Bug fixes
581
+ ---------
582
+ - Fixed an issue that led to incorrect ``GreatCircleICRSFrame`` transformations
583
+ when no ``ra0`` was provided.
584
+ - Fixed a bug in the ``OrphanKoposov19`` transformation.
585
+
586
+ API changes
587
+ -----------
588
+ - Overhauled the mock stellar stream generation methodology to allow for more
589
+ general stream generation. See ``MockStreamGenerator`` and the stream
590
+ distribution function classes, e.g., ``FardalStreamDF``.
591
+ - Removed deprecated ``CartesianPhaseSpacePosition`` class.
592
+ - Removed long deprecated ``Quaternion`` class.
593
+
594
+
595
+ 1.0 (2019-04-12)
596
+ ================
597
+
598
+ New Features
599
+ ------------
600
+ - Added a new coordinate frame for great circle coordinate systems defined by a
601
+ pole. This frame can be created with a pole and origin, a pole and longitude
602
+ zero point, by two points along a great circle, or by specifying the cartesian
603
+ basis vectors of the new frame.
604
+ - Added a function to transform a proper motion covariance matrix to a new
605
+ coordinate frame.
606
+ - Added support for compiling Gala with or without the GNU Scientific Library
607
+ (GSL), which is needed for the new potential classes indicated below.
608
+ - Added a new ``PowerLawCutoffPotential`` class for a power-law density
609
+ distribution with an exponential cutoff *(requires GSL)*.
610
+ - Added an implementation of the ``MWPotential2014`` from ``galpy`` (called
611
+ ``BovyMWPotential2014`` in ``gala``) *(requires GSL)*.
612
+ - Added an implementation of the Self-Consistent Field (SCF) basis function
613
+ expansion method for representing potential-density pairs *(requires GSL)*.
614
+ - Most Potential classes now support rotations and origin shifts through the
615
+ ``R`` and ``origin`` arguments.
616
+ - Added a ``progress`` argument to the Python integrators to display a progress
617
+ bar when stepping the integrators.
618
+ - When generating mock stellar streams and storing snapshots (rather than just
619
+ the final phase-space positions of the particles) now supports specifying the
620
+ snapshot frequency with the ``output_every`` argument.
621
+
622
+ Bug fixes
623
+ ---------
624
+ - Stream frames now properly wrap the longitude (``phi1``) components to the
625
+ range (-180, 180) deg.
626
+
627
+ API changes
628
+ -----------
629
+ - Stream classes have been renamed to reflect the author that defined them.
630
+ - Proper motion and coordinate velocity transformations have now been removed in
631
+ favor of the implementations in Astropy.
632
+ - Added a ``.data`` attribute to ``PhaseSpacePosition`` objects that returns a
633
+ ``Representation`` or ``NDRepresentation`` instance with velocity data
634
+ (differentials) attached.
635
+
636
+ 0.3 (2018-04-23)
637
+ ================
638
+
639
+ New Features
640
+ ------------
641
+
642
+ - Added a ``NullPotential`` class that has 0 mass and serves as a placeholder.
643
+ - Added a new ``zmax()`` method on the ``Orbit`` class to compute the maximum z
644
+ heights and times, or the mean maximum z height. Similar to ``apocenter()``
645
+ and ``pericenter()``.
646
+ - Added a new generator method on the ``Orbit`` class for easy iteration over
647
+ orbits.
648
+
649
+ Bug fixes
650
+ ---------
651
+
652
+ - ``Orbit.norbits`` now works...oops.
653
+ - ``apocenter()`` and ``pericenter()`` now work when more than one orbit is
654
+ stored in an ``Orbit`` class.
655
+
656
+ 0.2.2 (2017-10-07)
657
+ ==================
658
+
659
+ New features
660
+ ------------
661
+ - Added a new coordinate frame aligned with the Palomar 5 stream.
662
+ - Added a function ``gala.dynamics.combine`` to combine ``PhaseSpacePosition``
663
+ or ``Orbit`` objects.
664
+
665
+ Bug fixes
666
+ ---------
667
+ - Added a density function for the Kepler potential.
668
+ - Added a density function for the Long & Murali bar potential
669
+
670
+ Other changes
671
+ -------------
672
+ - Added JOSS paper.
673
+ - Cleaned up some tests and documentation to use the ``Hamiltonian`` object.
674
+
675
+ 0.2.1 (2017-07-19)
676
+ ==================
677
+
678
+ Bug fixes
679
+ ---------
680
+ - Array parameters are now numpy.ravel'd before being passed to the
681
+ ``CPotentialWrapper`` class methods.
682
+ - Added attribution to Bovy 2015 for disk potential in MilkyWayPotential
683
+
684
+ 0.2 (2017-07-15)
685
+ ================
686
+
687
+ New Features
688
+ ------------
689
+ - Added a new potential class for the Satoh density (Satoh 1980).
690
+ - Added support for Leapfrog integration when generating mock stellar streams.
691
+ - Added new colormaps and defaults for the matplotlib style.
692
+ - Added support for non-inertial reference frames and implemented a constant
693
+ rotating reference frame.
694
+ - Added a new class - ``Hamiltonian`` - for storing potentials with reference
695
+ frames. This should be used for easy orbit integration instead of the
696
+ potential classes.
697
+ - Added a new argument to the mock stream generation functions t output orbits
698
+ of all of the mock stream star particles to an HDF5 file.
699
+ - Cleaned up and simplified the process of subclassing a C-implemented
700
+ gravitational potential.
701
+ - Gravitational potential class instances can now be composed by just adding the
702
+ instances.
703
+ - Added a ``MilkyWayPotential`` class.
704
+
705
+ API-breaking changes
706
+ --------------------
707
+ - ``CartesianPhaseSpacePosition`` and ``CartesianOrbit`` are deprecated. Use
708
+ ``PhaseSpacePosition`` and ``Orbit`` with a Cartesian representation instead.
709
+ - Overhauled the storage of position and velocity information on
710
+ ``PhaseSpacePosition`` and ``Orbit`` classes. This uses new features in
711
+ Astropy 2.0 that allow attaching "differential" classes to representation
712
+ classes for storing velocity information. ``.pos`` and ``.vel`` no longer
713
+ point to arrays of Cartesian coordinates, but now instead point to
714
+ astropy.coordinates representation and differential objects, respectively.
715
+
716
+ 0.1.1 (2016-05-20)
717
+ ==================
718
+
719
+ - Removed debug statement.
720
+ - Added 'Why' page to documentation.
721
+
722
+ 0.1.0 (2016-05-19)
723
+ ==================
724
+
725
+ - Initial release.
gala/source/CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ All `gala` community members are expected to abide by the
2
+ [Astropy Project Code of Conduct](http://www.astropy.org/code_of_conduct.html).
gala/source/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2012-2024 Adrian M. Price-Whelan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gala/source/README.rst ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ |logo|
2
+
3
+ Gala is a Python package for Galactic and gravitational dynamics.
4
+
5
+ |Affiliated package| |Coverage Status| |Build status|
6
+
7
+ Documentation
8
+ -------------
9
+
10
+ |Documentation Status|
11
+
12
+ The documentation for ``Gala`` is hosted on `Read the docs
13
+ <http://gala.adrian.pw>`__.
14
+
15
+ Installation and Dependencies
16
+ -----------------------------
17
+
18
+ |PyPI|
19
+
20
+ The easiest way to get Gala is to install with pip or uv.
21
+
22
+ Gala can be installed with ``pip`` (with ``uv`` or standalone)::
23
+
24
+ pip install gala
25
+
26
+ You can also add ``gala`` as a dependency to your environment with::
27
+
28
+ uv add gala
29
+
30
+ See the `installation
31
+ instructions <http://gala.adrian.pw/en/latest/install.html>`_ in the
32
+ `documentation <http://gala.adrian.pw>`__ for more information.
33
+
34
+ Attribution
35
+ -----------
36
+
37
+ |JOSS| |DOI|
38
+
39
+ If you make use of this code, please cite the `JOSS <http://joss.theoj.org>`_
40
+ paper::
41
+
42
+ @article{gala,
43
+ doi = {10.21105/joss.00388},
44
+ url = {https://doi.org/10.21105%2Fjoss.00388},
45
+ year = 2017,
46
+ month = {oct},
47
+ publisher = {The Open Journal},
48
+ volume = {2},
49
+ number = {18},
50
+ author = {Adrian M. Price-Whelan},
51
+ title = {Gala: A Python package for galactic dynamics},
52
+ journal = {The Journal of Open Source Software}
53
+ }
54
+
55
+ Please also cite the Zenodo DOI |DOI| as a software citation - see the
56
+ `documentation
57
+ <http://gala.adrian.pw/en/latest/index.html#citation-and-attribution>`_ for up
58
+ to date citation information.
59
+
60
+ License
61
+ -------
62
+
63
+ |License|
64
+
65
+ Copyright 2013-2025 Adrian Price-Whelan and contributors.
66
+
67
+ ``Gala`` is free software made available under the MIT License. For details see
68
+ the `LICENSE <https://github.com/adrn/gala/blob/main/LICENSE>`_ file.
69
+
70
+ .. |Coverage Status| image:: https://codecov.io/gh/adrn/gala/branch/main/graph/badge.svg
71
+ :target: https://codecov.io/gh/adrn/gala
72
+ .. |Build status| image:: https://github.com/adrn/gala/actions/workflows/tests.yml/badge.svg
73
+ :target: https://github.com/adrn/gala/actions/workflows/tests.yml
74
+ .. |License| image:: http://img.shields.io/badge/license-MIT-blue.svg?style=flat
75
+ :target: https://github.com/adrn/gala/blob/main/LICENSE
76
+ .. |PyPI| image:: https://badge.fury.io/py/gala.svg
77
+ :target: https://badge.fury.io/py/gala
78
+ .. |conda| image:: https://anaconda.org/conda-forge/gala/badges/version.svg
79
+ :target: https://anaconda.org/conda-forge/gala
80
+ .. |Documentation Status| image:: https://readthedocs.org/projects/gala-astro/badge/?version=latest
81
+ :target: http://gala-astro.readthedocs.io/en/latest/?badge=latest
82
+ .. |Affiliated package| image:: https://img.shields.io/badge/astropy-affiliated%20package-orange.svg
83
+ :target: http://astropy.org/affiliated
84
+ .. |JOSS| image:: http://joss.theoj.org/papers/10.21105/joss.00388/status.svg
85
+ :target: http://joss.theoj.org/papers/10.21105/joss.00388
86
+ .. |DOI| image:: https://zenodo.org/badge/17577779.svg
87
+ :target: https://zenodo.org/badge/latestdoi/17577779
88
+ .. |ASCL| image:: https://img.shields.io/badge/ascl-1707.006-blue.svg?colorB=262255
89
+ :target: http://ascl.net/1707.006
90
+ .. |logo| image:: https://gala.adrian.pw/en/latest/_static/Gala_Logo_RGB.png
91
+ :target: https://github.com/adrn/gala
92
+ :width: 400
93
+
94
+ Contributors
95
+ ------------
96
+
97
+ See the `AUTHORS.rst <https://github.com/adrn/gala/blob/main/AUTHORS.rst>`_
98
+ file for a complete list of contributors to the project.
gala/source/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ gala Project Package Initialization File
4
+ """
gala/source/codemeta.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
3
+ "@type": "SoftwareSourceCode",
4
+ "name": "Gala: Galactic astronomy and gravitational dynamics",
5
+ "description": "Gala is an Astropy-affiliated Python package for galactic dynamics. Python enables wrapping low-level languages (e.g., C) for speed without losing flexibility or ease-of-use in the user-interface. The API for Gala was designed to provide a class-based and user-friendly interface to fast (C or Cython-optimized) implementations of common operations such as gravitational potential and force evaluation, orbit integration, dynamical transformations, and chaos indicators for nonlinear dynamics. Gala also relies heavily on and interfaces well with the implementations of physical units and astronomical coordinate systems in the Astropy package (astropy.units and astropy.coordinates).",
6
+ "identifier": "https://dx.doi.org/10.21105/joss.00388",
7
+ "author": [
8
+ {
9
+ "@type": "Person",
10
+ "givenName": "Adrian",
11
+ "familyName": "Price-Whelan",
12
+ "@id": "http://orcid.org/0000-0003-0872-7098"
13
+ }
14
+ ],
15
+ "citation": "https://ui.adsabs.harvard.edu/abs/2017JOSS....2..388P/abstract",
16
+ "relatedLink": [
17
+ "https://gala.adrian.pw/",
18
+ "https://zenodo.org/record/4159870"
19
+ ],
20
+ "codeRepository": ["https://github.com/adrn/gala"],
21
+ "version": "v1.3",
22
+ "license": "https://github.com/adrn/gala/blob/main/LICENSE"
23
+ }
gala/source/conftest.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from pytest_astropy_header.display import (
6
+ PYTEST_HEADER_MODULES,
7
+ TESTED_VERSIONS,
8
+ )
9
+
10
+ # Add test helpers to path so they can be imported
11
+ tests_dir = Path(__file__).parent
12
+ sys.path.insert(0, str(tests_dir))
13
+
14
+
15
+ def pytest_configure(config):
16
+ config.option.astropy_header = True
17
+ PYTEST_HEADER_MODULES.pop("Pandas", None)
18
+ PYTEST_HEADER_MODULES["astropy"] = "astropy"
19
+
20
+ from gala import __version__
21
+
22
+ packagename = os.path.basename(os.path.dirname(__file__))
23
+ TESTED_VERSIONS[packagename] = __version__
24
+
25
+
26
+ def pytest_report_header(config):
27
+ from gala._cconfig import EXP_ENABLED, GSL_ENABLED
28
+
29
+ hdr = []
30
+ if GSL_ENABLED:
31
+ hdr.append(" +++ Gala compiled with GSL +++")
32
+ else:
33
+ hdr.append(" --- Gala compiled without GSL ---")
34
+
35
+ if EXP_ENABLED:
36
+ hdr.append(" +++ Gala compiled with EXP +++")
37
+ else:
38
+ hdr.append(" --- Gala compiled without EXP ---")
39
+ hdr.append("")
40
+
41
+ return "\n".join(hdr)
gala/source/docs/Makefile ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line.
5
+ SPHINXOPTS =
6
+ SPHINXBUILD = sphinx-build
7
+ PAPER =
8
+ BUILDDIR = _build
9
+
10
+ # Internal variables.
11
+ PAPEROPT_a4 = -D latex_paper_size=a4
12
+ PAPEROPT_letter = -D latex_paper_size=letter
13
+ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14
+
15
+ .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
16
+
17
+ #This is needed with git because git doesn't create a dir if it's empty
18
+ $(shell [ -d "_static" ] || mkdir -p _static)
19
+
20
+ help:
21
+ @echo "Please use \`make <target>' where <target> is one of"
22
+ @echo " html to make standalone HTML files"
23
+ @echo " dirhtml to make HTML files named index.html in directories"
24
+ @echo " singlehtml to make a single large HTML file"
25
+ @echo " pickle to make pickle files"
26
+ @echo " json to make JSON files"
27
+ @echo " htmlhelp to make HTML files and a HTML help project"
28
+ @echo " qthelp to make HTML files and a qthelp project"
29
+ @echo " devhelp to make HTML files and a Devhelp project"
30
+ @echo " epub to make an epub"
31
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
32
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
33
+ @echo " text to make text files"
34
+ @echo " man to make manual pages"
35
+ @echo " changes to make an overview of all changed/added/deprecated items"
36
+ @echo " linkcheck to check all external links for integrity"
37
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
38
+
39
+ clean:
40
+ -rm -rf $(BUILDDIR)
41
+ -rm -rf api
42
+ -rm -rf tutorials/*.ipynb
43
+
44
+ html:
45
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
46
+ @echo
47
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
48
+
49
+ dirhtml:
50
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
51
+ @echo
52
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
53
+
54
+ singlehtml:
55
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
56
+ @echo
57
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
58
+
59
+ pickle:
60
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
61
+ @echo
62
+ @echo "Build finished; now you can process the pickle files."
63
+
64
+ json:
65
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
66
+ @echo
67
+ @echo "Build finished; now you can process the JSON files."
68
+
69
+ htmlhelp:
70
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
71
+ @echo
72
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
73
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
74
+
75
+ qthelp:
76
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
77
+ @echo
78
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
79
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
80
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Astropy.qhcp"
81
+ @echo "To view the help file:"
82
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Astropy.qhc"
83
+
84
+ devhelp:
85
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
86
+ @echo
87
+ @echo "Build finished."
88
+ @echo "To view the help file:"
89
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/Astropy"
90
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Astropy"
91
+ @echo "# devhelp"
92
+
93
+ epub:
94
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
95
+ @echo
96
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
97
+
98
+ latex:
99
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
100
+ @echo
101
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
102
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
103
+ "(use \`make latexpdf' here to do that automatically)."
104
+
105
+ latexpdf:
106
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
107
+ @echo "Running LaTeX files through pdflatex..."
108
+ make -C $(BUILDDIR)/latex all-pdf
109
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
110
+
111
+ text:
112
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
113
+ @echo
114
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
115
+
116
+ man:
117
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
118
+ @echo
119
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
120
+
121
+ changes:
122
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
123
+ @echo
124
+ @echo "The overview file is in $(BUILDDIR)/changes."
125
+
126
+ linkcheck:
127
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
128
+ @echo
129
+ @echo "Link check complete; look for any errors in the above output " \
130
+ "or in $(BUILDDIR)/linkcheck/output.txt."
131
+
132
+ doctest:
133
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
134
+ @echo "Testing of doctests in the sources finished, look at the " \
135
+ "results in $(BUILDDIR)/doctest/output.txt."
136
+
137
+ exectutorials:
138
+ jupytext --to ipynb --execute tutorials/*.py
139
+ @echo "Finished executing tutorial notebooks. Look at the executed " \
140
+ "notebooks in tutorials/"
141
+
142
+ execsupporting:
143
+ jupytext --to ipynb --execute supporting/*.py
144
+ @echo "Finished executing tutorial notebooks. Look at the executed " \
145
+ "notebooks in supporting/"
146
+
147
+ animations:
148
+ python _static_animations.py
149
+ @echo "Finished generating animation files"
gala/source/docs/_static/Gala_Logo_RGB.png ADDED
gala/source/docs/_static/anim-prof.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55fdb6e46d3637fe2264f55310905faeef1b10dd4117b3ab917d7e5d728d7642
3
+ size 184605
gala/source/docs/_static/gala.css ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ span#logotext2 {
2
+ color: #764099;
3
+ }
4
+
5
+ /* Taken from NumPy */
6
+
7
+ @import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;0,900;1,400;1,700;1,900&family=Open+Sans:ital,wght@0,400;0,600;1,400;1,600&display=swap");
8
+
9
+ .navbar-brand img {
10
+ height: 60px;
11
+ }
12
+ .navbar-brand {
13
+ height: 75px;
14
+ }
15
+
16
+ body {
17
+ font-family: "Open Sans", sans-serif;
18
+ }
19
+
20
+ pre,
21
+ code {
22
+ font-size: 100%;
23
+ line-height: 155%;
24
+ }
25
+
26
+ div.output_area div[class*="highlight"] pre {
27
+ white-space: pre-wrap;
28
+ }
29
+
30
+ /* Make output lighter gray and no italics for the love of all that is holy! */
31
+ html[data-theme="light"] .highlight .go {
32
+ color: #555555;
33
+ font-style: normal;
34
+ }
35
+
36
+ html[data-theme="dark"] .highlight .go {
37
+ font-style: normal;
38
+ }
39
+
40
+ /* OMG why would you bold numbers */
41
+ .highlight .mf,
42
+ .highlight .mi {
43
+ font-weight: 300;
44
+ }
45
+
46
+ /* Override some aspects of the pydata-sphinx-theme: taken from Pandas */
47
+
48
+ :root {
49
+ /* Use softer blue from bootstrap's default info color */
50
+ /* --pst-color-info: 23, 162, 184; */
51
+ --pst-color-primary: 118, 63, 152;
52
+ --pst-color-success: 40, 167, 69;
53
+ --pst-color-info: 0, 123, 255;
54
+ --pst-color-warning: 255, 193, 7;
55
+ --pst-color-danger: 220, 53, 69;
56
+ --pst-color-text-base: 51, 51, 51;
57
+
58
+ --pst-font-size-base: 15px;
59
+
60
+ --pst-color-link: 118, 63, 152;
61
+ --pst-color-headerlink-hover: 118, 63, 152;
62
+
63
+ /* heading font sizes */
64
+ --pst-font-size-h1: 28px;
65
+ --pst-font-size-h2: 25px;
66
+ --pst-font-size-h3: 20px;
67
+ --pst-font-size-h4: 18px;
68
+ --pst-font-size-h5: 16px;
69
+ --pst-font-size-h6: 15px;
70
+ }
gala/source/docs/_static/m104.ico ADDED
gala/source/docs/_static/orbit-anim1.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13306d74201810520823f51478e8724c4d5cbc7b960ad9b1d939872c9a38319f
3
+ size 364400
gala/source/docs/_static/orbit-anim2.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ac8b5bbb111753d6b13d5918b889c9bf6a03a0dbeba75a1297f22cfc93260f8d
3
+ size 257682
gala/source/docs/_static_animations.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def make_orbit_animations(static_path):
2
+ # orbits-in-derail.rst
3
+ import astropy.units as u
4
+
5
+ import gala.dynamics as gd
6
+ import gala.potential as gp
7
+ from gala.units import galactic
8
+
9
+ file1 = static_path / "orbit-anim1.mp4"
10
+ file2 = static_path / "orbit-anim2.mp4"
11
+
12
+ if file1.exists() and file2.exists():
13
+ return
14
+
15
+ pot = gp.PlummerPotential(m=1e10 * u.Msun, b=1.0 * u.kpc, units=galactic)
16
+ w0 = gd.PhaseSpacePosition(pos=[2.0, 0, 0] * u.kpc, vel=[0.0, 75, 15] * u.km / u.s)
17
+ orbit = gp.Hamiltonian(pot).integrate_orbit(w0, dt=1.0, n_steps=5000)
18
+
19
+ # animation 1:
20
+ fig, anim = orbit[:1000].animate(stride=10)
21
+ anim.save(file1)
22
+
23
+ # animation 2:
24
+ _fig, anim = orbit[:1000].cylindrical.animate(components=["rho", "z"], stride=10)
25
+ anim.save(file2)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ import pathlib
30
+
31
+ make_orbit_animations(pathlib.Path("./_static").resolve().absolute())
gala/source/docs/_templates/autosummary/base.rst ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if referencefile %}
2
+ .. include:: {{ referencefile }}
3
+ {% endif %}
4
+
5
+ {{ objname }}
6
+ {{ underline }}
7
+
8
+ .. currentmodule:: {{ module }}
9
+
10
+ .. auto{{ objtype }}:: {{ objname }}
gala/source/docs/_templates/autosummary/class.rst ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if referencefile %}
2
+ .. include:: {{ referencefile }}
3
+ {% endif %}
4
+
5
+ {{ objname }}
6
+ {{ underline }}
7
+
8
+ .. currentmodule:: {{ module }}
9
+
10
+ .. autoclass:: {{ objname }}
11
+ :show-inheritance:
12
+
13
+ {% if '__init__' in methods %}
14
+ {% set caught_result = methods.remove('__init__') %}
15
+ {% endif %}
16
+
17
+ {% block attributes_summary %}
18
+ {% if attributes %}
19
+
20
+ .. rubric:: Attributes Summary
21
+
22
+ .. autosummary::
23
+ {% for item in attributes %}
24
+ ~{{ name }}.{{ item }}
25
+ {%- endfor %}
26
+
27
+ {% endif %}
28
+ {% endblock %}
29
+
30
+ {% block methods_summary %}
31
+ {% if methods %}
32
+
33
+ .. rubric:: Methods Summary
34
+
35
+ .. autosummary::
36
+ {% for item in methods %}
37
+ ~{{ name }}.{{ item }}
38
+ {%- endfor %}
39
+
40
+ {% endif %}
41
+ {% endblock %}
42
+
43
+ {% block attributes_documentation %}
44
+ {% if attributes %}
45
+
46
+ .. rubric:: Attributes Documentation
47
+
48
+ {% for item in attributes %}
49
+ .. autoattribute:: {{ item }}
50
+ {%- endfor %}
51
+
52
+ {% endif %}
53
+ {% endblock %}
54
+
55
+ {% block methods_documentation %}
56
+ {% if methods %}
57
+
58
+ .. rubric:: Methods Documentation
59
+
60
+ {% for item in methods %}
61
+ .. automethod:: {{ item }}
62
+ {%- endfor %}
63
+
64
+ {% endif %}
65
+ {% endblock %}
gala/source/docs/_templates/autosummary/module.rst ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if referencefile %}
2
+ .. include:: {{ referencefile }}
3
+ {% endif %}
4
+
5
+ {{ objname }}
6
+ {{ underline }}
7
+
8
+ .. automodule:: {{ fullname }}
9
+
10
+ {% block functions %}
11
+ {% if functions %}
12
+ .. rubric:: Functions
13
+
14
+ .. autosummary::
15
+ {% for item in functions %}
16
+ {{ item }}
17
+ {%- endfor %}
18
+ {% endif %}
19
+ {% endblock %}
20
+
21
+ {% block classes %}
22
+ {% if classes %}
23
+ .. rubric:: Classes
24
+
25
+ .. autosummary::
26
+ {% for item in classes %}
27
+ {{ item }}
28
+ {%- endfor %}
29
+ {% endif %}
30
+ {% endblock %}
31
+
32
+ {% block exceptions %}
33
+ {% if exceptions %}
34
+ .. rubric:: Exceptions
35
+
36
+ .. autosummary::
37
+ {% for item in exceptions %}
38
+ {{ item }}
39
+ {%- endfor %}
40
+ {% endif %}
41
+ {% endblock %}
gala/source/docs/conf.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import os
3
+ import pathlib
4
+ import re
5
+ import sys
6
+ import warnings
7
+ from importlib import import_module
8
+
9
+ # Load all of the global Astropy configuration
10
+ try:
11
+ from sphinx_astropy.conf.v1 import * # noqa: F403
12
+ except ImportError:
13
+ print(
14
+ "ERROR: Building the documentation for Gala requires the "
15
+ "sphinx-astropy package to be installed"
16
+ )
17
+ sys.exit(1)
18
+
19
+ # Get configuration information from setup.cfg
20
+ from configparser import ConfigParser
21
+
22
+ conf = ConfigParser()
23
+
24
+ docs_root = pathlib.Path(__file__).parent.resolve()
25
+
26
+ # -- General configuration ----------------------------------------------------
27
+
28
+ # By default, highlight as Python 3.
29
+ highlight_language = "python3"
30
+
31
+ # List of patterns, relative to source directory, that match files and
32
+ # directories to ignore when looking for source files.
33
+ exclude_patterns = ["_build", "**.ipynb_checkpoints"]
34
+
35
+ # Add any paths that contain templates here, relative to this directory.
36
+ templates_path = ["_templates"]
37
+
38
+ # The suffix(es) of source filenames.
39
+ # You can specify multiple suffix as a list of string:
40
+ # source_suffix = ['.rst', '.md']
41
+ source_suffix = ".rst"
42
+
43
+ # Don't show summaries of the members in each class along with the
44
+ # class' docstring
45
+ numpydoc_show_class_members = False
46
+
47
+ # Whether to create cross-references for the parameter types in the
48
+ # Parameters, Other Parameters, Returns and Yields sections of the docstring.
49
+ numpydoc_xref_param_type = True
50
+
51
+ autosummary_generate = True
52
+
53
+ automodapi_toctreedirnm = "api"
54
+
55
+ # The reST default role (used for this markup: `text`) to use for all
56
+ # documents. Set to the "smart" one.
57
+ default_role = "obj"
58
+
59
+ # Class documentation should contain *both* the class docstring and
60
+ # the __init__ docstring
61
+ autoclass_content = "both"
62
+
63
+ # This is added to the end of RST files - a good place to put substitutions to
64
+ # be used globally.
65
+ rst_epilog = """
66
+ """
67
+
68
+ # intersphinx
69
+ intersphinx_mapping = {
70
+ "python": ("https://docs.python.org/3/", None),
71
+ "numpy": ("https://numpy.org/doc/stable/", None),
72
+ "scipy": ("https://docs.scipy.org/doc/scipy/", None),
73
+ "matplotlib": ("https://matplotlib.org/stable/", None),
74
+ "astropy": ("https://docs.astropy.org/en/stable/", None),
75
+ "h5py": ("https://docs.h5py.org/en/stable/", None),
76
+ "sympy": ("https://docs.sympy.org/latest/", None),
77
+ }
78
+
79
+ # Show / hide TODO blocks
80
+ todo_include_todos = True
81
+
82
+ # -- Project information ------------------------------------------------------
83
+
84
+ # This does not *have* to match the package name, but typically does
85
+ project = "gala"
86
+ author = "Adrian Price-Whelan"
87
+ copyright = f"{datetime.datetime.now().year}, {author}"
88
+
89
+ package_name = "gala"
90
+ import_module(package_name)
91
+ package = sys.modules[package_name]
92
+
93
+ plot_formats = [("png", 200), ("pdf", 200)]
94
+ plot_apply_rcparams = True
95
+ # NOTE: if you update these, also update docs/tutorials/nb_setup
96
+ plot_rcparams = {
97
+ "image.cmap": "magma",
98
+ # Fonts:
99
+ "font.size": 16,
100
+ "figure.titlesize": "x-large",
101
+ "axes.titlesize": "large",
102
+ "axes.labelsize": "large",
103
+ "xtick.labelsize": "medium",
104
+ "ytick.labelsize": "medium",
105
+ # Axes:
106
+ "axes.labelcolor": "k",
107
+ "axes.axisbelow": True,
108
+ # Ticks
109
+ "xtick.color": "#333333",
110
+ "xtick.direction": "in",
111
+ "ytick.color": "#333333",
112
+ "ytick.direction": "in",
113
+ "xtick.top": True,
114
+ "ytick.right": True,
115
+ "figure.dpi": 300,
116
+ "savefig.dpi": 300,
117
+ }
118
+ plot_include_source = False
119
+
120
+ # The short X.Y version.
121
+ version = package.__version__.split("-", 1)[0]
122
+ # The full version, including alpha/beta/rc tags.
123
+ release = package.__version__
124
+
125
+
126
+ # -- Options for HTML output ---------------------------------------------------
127
+
128
+ html_theme = "pydata_sphinx_theme"
129
+ html_logo = "_static/Gala_Logo_RGB.png"
130
+
131
+ html_theme_options = {
132
+ "logo": {
133
+ "image_light": "Gala_Logo_RGB.png",
134
+ "image_dark": "Gala_Logo_RGB.png",
135
+ },
136
+ "icon_links": [
137
+ {
138
+ "name": "GitHub",
139
+ "url": "https://github.com/adrn/gala",
140
+ "icon": "fab fa-github-square",
141
+ },
142
+ {
143
+ "name": "Twitter",
144
+ "url": "https://twitter.com/adrianprw",
145
+ "icon": "fab fa-twitter-square",
146
+ },
147
+ ],
148
+ }
149
+
150
+ # Add any paths that contain custom themes here, relative to this directory.
151
+ # To use a different custom theme, add the directory containing the theme.
152
+ # html_theme_path = ['_themes/sphinx_rtd_theme']
153
+
154
+ # The theme to use for HTML and HTML Help pages. See the documentation for
155
+ # a list of builtin themes. To override the custom theme, set this to the
156
+ # name of a builtin theme or the name of a custom theme in html_theme_path.
157
+ # html_theme = "sphinx_rtd_theme"
158
+
159
+ # Custom sidebar templates, maps document names to template names.
160
+ html_sidebars = {"**": ["search-field.html", "sidebar-nav-bs.html"]}
161
+
162
+ # The name of an image file (within the static path) to use as favicon of the
163
+ # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
164
+ # pixels large.
165
+ html_favicon = str(docs_root / "_static" / "m104.ico")
166
+
167
+ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
168
+ # using the given strftime format.
169
+ # html_last_updated_fmt = ''
170
+
171
+ # The name for this set of Sphinx documents. If None, it defaults to
172
+ # "<project> v<release> documentation".
173
+ html_title = f"{project} v{release}"
174
+
175
+ # Output file base name for HTML help builder.
176
+ htmlhelp_basename = project + "doc"
177
+
178
+ # Static files to copy after template files
179
+ html_static_path = ["_static"]
180
+ html_css_files = ["gala.css"]
181
+
182
+
183
+ # -- Options for LaTeX output --------------------------------------------------
184
+
185
+ # Grouping the document tree into LaTeX files. List of tuples
186
+ # (source start file, target name, title, author, documentclass [howto/manual]).
187
+ latex_documents = [
188
+ ("index", project + ".tex", project + " Documentation", author, "manual")
189
+ ]
190
+
191
+ # show inherited members for classes
192
+ automodsumm_inherited_members = True
193
+
194
+ # Add nbsphinx
195
+ extensions += [ # noqa: F405
196
+ "nbsphinx",
197
+ "IPython.sphinxext.ipython_console_highlighting",
198
+ "sphinxcontrib.bibtex",
199
+ "rtds_action",
200
+ ]
201
+
202
+ # Bibliography:
203
+ bibtex_bibfiles = ["refs.bib"]
204
+ bibtex_reference_style = "author_year"
205
+
206
+ # Custom setting for nbsphinx - timeout for executing one cell
207
+ nbsphinx_timeout = 300
208
+ nbsphinx_kernel_name = os.environ.get("NBSPHINX_KERNEL_NAME", "python3")
209
+
210
+ # nbsphinx hacks (thanks exoplanet)
211
+ import nbsphinx # noqa: E402
212
+ from nbsphinx import markdown2rst as original_markdown2rst # noqa: E402
213
+
214
+ nbsphinx.RST_TEMPLATE = nbsphinx.RST_TEMPLATE.replace(
215
+ "{%- if width %}", "{%- if 0 %}"
216
+ ).replace("{%- if height %}", "{%- if 0 %}")
217
+
218
+
219
+ def subber(m):
220
+ return m.group(0).replace("``", "`")
221
+
222
+
223
+ prog = re.compile(r":(.+):``(.+)``")
224
+
225
+
226
+ def markdown2rst(text):
227
+ return prog.sub(subber, original_markdown2rst(text))
228
+
229
+
230
+ nbsphinx.markdown2rst = markdown2rst
231
+
232
+ # rtds-action
233
+ if "GITHUB_TOKEN" in os.environ:
234
+ print("GitHub Token found: retrieving artifact")
235
+
236
+ # The name of your GitHub repository
237
+ rtds_action_github_repo = "adrn/gala"
238
+
239
+ # The path where the artifact should be extracted
240
+ # Note: this is relative to the conf.py file!
241
+ rtds_action_path = "."
242
+
243
+ # The "prefix" used in the `upload-artifact` step of the action
244
+ rtds_action_artifact_prefix = "notebooks-for-"
245
+
246
+ # A GitHub personal access token is required, more info below
247
+ rtds_action_github_token = os.environ["GITHUB_TOKEN"]
248
+
249
+ # Whether or not to raise an error on ReadTheDocs if the
250
+ # artifact containing the notebooks can't be downloaded (optional)
251
+ rtds_action_error_if_missing = True
252
+
253
+ else:
254
+ rtds_action_github_repo = ""
255
+ rtds_action_github_token = ""
256
+ rtds_action_path = ""
257
+
258
+ ## -- Retrieve Zenodo record for most recent version of Gala:
259
+ zenodo_path = docs_root / "ZENODO.rst"
260
+ if not zenodo_path.exists():
261
+ import textwrap
262
+
263
+ try:
264
+ import requests
265
+
266
+ headers = {"accept": "application/x-bibtex"}
267
+ response = requests.get(
268
+ "https://zenodo.org/api/records/16923466", headers=headers
269
+ )
270
+ response.encoding = "utf-8"
271
+ zenodo_record = ".. code-block:: bibtex\n\n" + textwrap.indent(
272
+ response.text, " " * 4
273
+ )
274
+ except Exception as e:
275
+ warnings.warn(f"Failed to retrieve Zenodo record for Gala: {e!s}", stacklevel=1)
276
+ zenodo_record = (
277
+ "`Retrieve the Zenodo record here <https://zenodo.org/record/16923466>`_"
278
+ )
279
+
280
+ with open(zenodo_path, "w", encoding="utf-8") as f:
281
+ f.write(zenodo_record)
282
+
283
+ ## -- Check for executed tutorials and only add to toctree if they exist:
284
+
285
+ # Note: for jupytext tutorials (the .py files), put the expected (generated) .ipynb name
286
+ tutorial_files = [
287
+ "tutorials/Milky-Way-model.ipynb",
288
+ "tutorials/integrate-potential-example.rst",
289
+ "tutorials/pyia-gala-orbit.ipynb",
290
+ "tutorials/integrate-barred-potential.ipynb",
291
+ "tutorials/mock-stream-heliocentric.rst",
292
+ "tutorials/circ-restricted-3body.rst",
293
+ "tutorials/spherical-spline-tutorial.ipynb",
294
+ "tutorials/Arbitrary-density-SCF.ipynb",
295
+ "tutorials/exp.rst",
296
+ "tutorials/stream-mass-loss.ipynb",
297
+ "tutorials/time-evolving-potential.ipynb",
298
+ "tutorials/v1_11_new_features.ipynb",
299
+ # Supporting documents:
300
+ "supporting/define-milky-way-model.ipynb",
301
+ ]
302
+
303
+ _not_executed = []
304
+ _tutorial_toctree_items = []
305
+ _supporting_toctree_items = []
306
+ for fn in tutorial_files:
307
+ if not pathlib.Path(fn).exists() and "GITHUB_TOKEN" not in os.environ:
308
+ _not_executed.append(fn)
309
+ continue
310
+
311
+ if fn.startswith("supporting/"):
312
+ _supporting_toctree_items.append(fn)
313
+ elif fn.startswith("tutorials/"):
314
+ _tutorial_toctree_items.append(fn)
315
+
316
+ if _tutorial_toctree_items:
317
+ _tutorial_toctree_items = "\n ".join(_tutorial_toctree_items)
318
+ _tutorial_toctree = f"""
319
+ .. toctree::
320
+ :maxdepth: 1
321
+ :glob:
322
+
323
+ {_tutorial_toctree_items}
324
+ """
325
+
326
+ else:
327
+ _tutorial_toctree = "No tutorials found!"
328
+
329
+ if _supporting_toctree_items:
330
+ _supporting_toctree_items = "\n ".join(_supporting_toctree_items)
331
+ _supporting_toctree = f"""
332
+ .. toctree::
333
+ :maxdepth: 1
334
+ :glob:
335
+
336
+ {_supporting_toctree_items}
337
+ """
338
+
339
+ else:
340
+ _supporting_toctree = "No supporting documents found!"
341
+
342
+ if _not_executed:
343
+ print(
344
+ "\n-------- Gala warning --------\n"
345
+ "Some tutorial notebooks could not be found! This is likely because "
346
+ "the tutorial notebooks have not been executed. If you are building "
347
+ "the documentation locally, you may want to run 'make exectutorials' "
348
+ "before running the sphinx build."
349
+ )
350
+ print(f"Missing tutorials: {', '.join(_not_executed)}\n")
351
+
352
+ with open("_tutorials.rst", "w", encoding="utf-8") as f:
353
+ f.write(_tutorial_toctree)
354
+
355
+ with open("_supporting.rst", "w", encoding="utf-8") as f:
356
+ f.write(_supporting_toctree)
gala/source/docs/contributing.rst ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. include:: references.txt
2
+
3
+ *****************
4
+ How to contribute
5
+ *****************
6
+
7
+ We welcome contributions from anyone via pull requests on `GitHub
8
+ <https://github.com/adrn/gala>`_. If you don't feel comfortable modifying or
9
+ adding functionality, we also welcome feature requests and bug reports as
10
+ `GitHub issues <https://github.com/adrn/gala/issues>`_.
11
+
12
+ Developer documentation
13
+ =======================
14
+
15
+ .. toctree::
16
+ :maxdepth: 1
17
+
18
+ testing
19
+ docs
gala/source/docs/conventions.rst ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ .. _conventions:
3
+
4
+ ***********
5
+ Conventions
6
+ ***********
7
+
8
+ .. _name-conventions:
9
+
10
+ Common variable names
11
+ =====================
12
+
13
+ This package uses standard variable names throughout for consistency:
14
+
15
+ - ``w`` represents phase-space coordinates (positions and velocities)
16
+ - ``q`` represents positions only
17
+ - ``p`` or ``v`` represent velocities or momenta
18
+ - ``t`` represents time arrays
19
+
20
+ .. _shape-conventions:
21
+
22
+ Array shapes
23
+ ============
24
+
25
+ Arrays and :class:`~astropy.units.Quantity` objects in ``Gala`` follow
26
+ consistent shape conventions:
27
+
28
+ **Coordinate arrays**: ``axis=0`` is the coordinate dimension. For example,
29
+ 128 different 3D Cartesian positions have shape ``(3, 128)``.
30
+
31
+ **Orbit collections**: Arrays have three axes:
32
+ - ``axis=0``: coordinate dimension
33
+ - ``axis=1``: time axis
34
+ - ``axis=2``: different orbits
35
+
36
+ .. _energy-momentum:
37
+
38
+ Energy and momentum
39
+ ===================
40
+
41
+ In `gala`, energy and angular momentum quantities are *per unit mass* unless
42
+ otherwise specified. This applies to:
43
+
44
+ - Potential energy
45
+ - Kinetic energy
46
+ - Total energy
47
+ - Angular momentum
48
+ - Linear momentum
49
+ - Conjugate momenta
gala/source/docs/coordinates/greatcircle.rst ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ For the examples below, we assume the following imports have already been
2
+ executed::
3
+
4
+ >>> import astropy.units as u
5
+ >>> import astropy.coordinates as coord
6
+ >>> import numpy as np
7
+ >>> import gala.coordinates as gc
8
+
9
+ .. _greatcircle:
10
+
11
+ *************************************************
12
+ Great circle and stellar stream coordinate frames
13
+ *************************************************
14
+
15
+
16
+ Introduction
17
+ ============
18
+
19
+ Great circle coordinate systems are defined as a rotation from another spherical
20
+ coordinate system, such as the ICRS. The great circle system is defined by a specified
21
+ (north) pole and spherical origin -- i.e. a specification of the new coordinate system x
22
+ and z axes in components of the old coordinate system.
23
+
24
+ `gala` currently supports great circle frames that are defined as a rotation away from
25
+ the ICRS (RA, Dec) through the `~gala.coordinates.GreatCircleICRSFrame` class. To create
26
+ a new great circle frame with the default initializer, you must specify a pole using the
27
+ ``pole`` keyword argument and the spherical origin with the ``origin`` argument.
28
+ However, this frame also supports other initialization paths through the ``from_``
29
+ classmethods (see API below). These classmethods are the most useful initialization
30
+ methods. For example, to define a great circle system with the pole at (RA, Dec) =
31
+ (32.5, 19.8)º and a longitude 0 at RA=100º, we first have to create a coordinate object
32
+ for the pole::
33
+
34
+ >>> pole = coord.SkyCoord(ra=32.5*u.deg, dec=19.8*u.deg)
35
+
36
+ We can then pass this pole to the `~gala.coordinates.GreatCircleICRSFrame.from_pole_ra0`
37
+ classmethod to define our coordinate frame::
38
+
39
+ >>> frame = gc.GreatCircleICRSFrame.from_pole_ra0(pole=pole, ra0=100*u.deg)
40
+
41
+ This frame instance acts like any other Astropy coordinate frame. For example, we can
42
+ transform other coordinates to this new coordinate system using::
43
+
44
+ >>> c = coord.SkyCoord(ra=[160, 53]*u.deg, dec=[-11, 9]*u.deg)
45
+ >>> c_fr = c.transform_to(frame)
46
+ >>> c_fr # doctest: +FLOAT_CMP
47
+ <SkyCoord (GreatCircleICRSFrame: pole=<ICRS Coordinate: (ra, dec) in deg
48
+ (32.5, 19.8)>, origin=<ICRS Coordinate: (ra, dec) in deg
49
+ (280., 46.74765478)>, priority=origin): (phi1, phi2) in deg
50
+ [(-127.59199268, -38.82050866), (-154.93887946, 67.43382209)]>
51
+
52
+ The spherical coordinate components of the resulting great circle frame are
53
+ always named ``phi1`` and ``phi2``, so to access the longitude and latitude in
54
+ the new system, we use::
55
+
56
+ >>> c_fr.phi1 # doctest: +FLOAT_CMP
57
+ <Longitude [-127.59199268, -154.93887946] deg>
58
+ >>> c_fr.phi2 # doctest: +FLOAT_CMP
59
+ <Latitude [-38.82050866, 67.43382209] deg>
60
+
61
+ The transformation also works for velocity components. For example, if we have a
62
+ sky position and proper motions, we can transform to the great circle frame in
63
+ the same way::
64
+
65
+ >>> c2 = coord.SkyCoord(
66
+ ... ra=160*u.deg,
67
+ ... dec=-11*u.deg,
68
+ ... pm_ra_cosdec=5*u.mas/u.yr,
69
+ ... pm_dec=0.3*u.mas/u.yr
70
+ ... )
71
+ >>> c2_fr = c2.transform_to(frame)
72
+ >>> c2_fr.phi1 # doctest: +FLOAT_CMP
73
+ <Longitude -127.59199268 deg>
74
+ >>> c2_fr.pm_phi1_cosphi2 # doctest: +FLOAT_CMP
75
+ <Quantity 1.71997614 mas / yr>
76
+ >>> c2_fr.pm_phi2 # doctest: +FLOAT_CMP
77
+ <Quantity -4.70443217 mas / yr>
78
+
79
+ The generic great circle frame can also handle transforming from great circle
80
+ coordinates to other coordinate frames. For example, to transform a grid of points along
81
+ a great circle to the ICRS system, we would define a frame with positional data and a
82
+ specified pole::
83
+
84
+ >>> c3_fr = gc.GreatCircleICRSFrame(
85
+ ... phi1=np.linspace(0, 360, 8)*u.deg,
86
+ ... phi2=0*u.deg,
87
+ ... pole=frame.pole,
88
+ ... origin=frame.origin
89
+ ... )
90
+ >>> c3 = c3_fr.transform_to(coord.ICRS())
91
+ >>> c3.ra # doctest: +FLOAT_CMP
92
+ <Longitude [280. , 302.73861084, 326.04009238, 67.95460569,
93
+ 113.51995793, 132.05271289, 180.05477998, 280. ] deg>
94
+
95
+
96
+ Creating a coordinate frame from two points along a great circle
97
+ ================================================================
98
+
99
+ It is sometimes convenient to define a great circle coordinate frame by specifying two
100
+ endpoints of an arc segment along a great circle (instead of the pole). For these use
101
+ cases, the `~gala.coordinates.GreatCircleICRSFrame.from_endpoints` provides a
102
+ convenience classmethod for creating a great circle frame with endpoints::
103
+
104
+ >>> endpoints = coord.SkyCoord(
105
+ ... ra=[-38.8, 4.7]*u.deg,
106
+ ... dec=[-45.1, -51.7]*u.deg
107
+ ... )
108
+ >>> frame2 = gc.GreatCircleICRSFrame.from_endpoints(endpoints[0], endpoints[1])
109
+ >>> frame2
110
+ <GreatCircleICRSFrame Frame (pole=<ICRS Coordinate: (ra, dec) in deg
111
+ (359.1291976, 38.16814051)>, origin=<ICRS Coordinate: (ra, dec) in deg
112
+ (341.46580563, -50.48035324)>, priority=origin)>
113
+
114
+ Without specifying a longitude zeropoint, the default behavior of the above classmethod
115
+ is to take the spherical midpoint of the two endpoints as the longitude zeropoint.
116
+ However, a custom zeropoint can be specified using the ``ra0`` keyword argument. For
117
+ example::
118
+
119
+ >>> frame3 = gc.GreatCircleICRSFrame.from_endpoints(
120
+ ... endpoints[0], endpoints[1], ra0=150*u.deg
121
+ ... )
122
+ >>> frame3
123
+ <GreatCircleICRSFrame Frame (pole=<ICRS Coordinate: (ra, dec) in deg
124
+ (359.1291976, 38.16814051)>, origin=<ICRS Coordinate: (ra, dec) in deg
125
+ (330., -48.01820335)>, priority=origin)>
126
+
127
+
128
+ Creating a coordinate frame from endpoints and an origin
129
+ ========================================================
130
+
131
+ When working with stellar streams, it is sometimes useful to create a stream-aligned
132
+ coordinate frame by specifying an exact origin for the new great circle coordinate frame
133
+ (e.g., set to the progenitor system) along with the endpoints of the stream (which are
134
+ often close to defining a great circle). In these cases, the great circle defined by the
135
+ endpoints and the great circle defined by the origin may not be orthogonal. You can
136
+ still use these to create a `~gala.coordinates.GreatCircleICRSFrame`, but by default the
137
+ pole location will be adjusted to be orthogonal to the input origin::
138
+
139
+ >>> endpoints = coord.SkyCoord(
140
+ ... ra=[-38.8, 4.7]*u.deg,
141
+ ... dec=[-45.1, -51.7]*u.deg
142
+ ... )
143
+ >>> origin = coord.SkyCoord(330., -48., unit=u.deg)
144
+ >>> frame4 = gc.GreatCircleICRSFrame.from_endpoints( # doctest: +IGNORE_WARNINGS
145
+ ... endpoints[0], endpoints[1], origin=origin
146
+ ... )
147
+ >>> frame4
148
+ <GreatCircleICRSFrame Frame (pole=<ICRS Coordinate: (ra, dec) in deg
149
+ (359.13616655, 38.18404071)>, origin=<ICRS Coordinate: (ra, dec) in deg
150
+ (330., -48.)>, priority=origin)>
151
+
152
+
153
+ Creating a coordinate frame from a pole and longitude zero point
154
+ ================================================================
155
+
156
+ Another common way of initializing great circle coordinate systems is with a pole and a
157
+ longitude zero point (as was previously — prior to v1.7 — allowed in the initializer
158
+ `~gala.coordinates.GreatCircleICRSFrame`). This can now be done with the
159
+ `~gala.coordinates.GreatCircleICRSFrame.from_pole_ra0` classmethod::
160
+
161
+ >>> frame5 = gc.GreatCircleICRSFrame.from_pole_ra0(
162
+ ... pole=pole, ra0=100*u.deg
163
+ ... )
164
+ >>> frame5
165
+ <GreatCircleICRSFrame Frame (pole=<ICRS Coordinate: (ra, dec) in deg
166
+ (32.5, 19.8)>, origin=<ICRS Coordinate: (ra, dec) in deg
167
+ (280., 46.74765478)>, priority=origin)>
168
+
169
+ With just these inputs, there is an ambiguity in the definition of the coordinate frame
170
+ because the great circles defined by the pole and longitude zero point intersect at two
171
+ locations (so there are two possible origins, one being the negative of the other). The
172
+ convention here is to pick the origin closest to (0, 0). To have finer control over
173
+ which origin is picked, you can also pass in a sky coordinate object with the
174
+ ``origin_disambiguate`` argument, and the origin closest to this coordinate will be used
175
+ to define the coordinate frame.
176
+
177
+
178
+ .. _greatcircle-api:
179
+
180
+ API
181
+ ===
182
+
183
+ .. automodapi:: gala.coordinates.greatcircle
184
+ :no-inheritance-diagram:
gala/source/docs/coordinates/index.rst ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. module:: gala.coordinates
2
+
3
+ .. _gala-coordinates:
4
+
5
+ *********************************************
6
+ Coordinate Systems (`gala.coordinates`)
7
+ *********************************************
8
+
9
+ Introduction
10
+ ============
11
+
12
+ The `~gala.coordinates` subpackage primarily provides specialty
13
+ :mod:`astropy.coordinates` frame classes for coordinate systems defined by the
14
+ stellar streams, and for other common Galactic dynamics tasks like removing
15
+ solar reflex motion from proper motions or radial velocities, and transforming
16
+ a proper motion covariance matrix from one frame to another.
17
+
18
+ For the examples below the following imports have already been executed::
19
+
20
+ >>> import numpy as np
21
+ >>> import astropy.coordinates as coord
22
+ >>> import astropy.units as u
23
+ >>> import gala.coordinates as gc
24
+
25
+ We will also set the default Astropy Galactocentric frame parameters to the
26
+ values adopted in Astropy v4.0:
27
+
28
+ >>> _ = coord.galactocentric_frame_defaults.set('v4.0')
29
+
30
+ Stellar stream coordinate frames
31
+ ================================
32
+
33
+ `gala` provides Astropy coordinate frame classes for transforming to several
34
+ built-in stellar stream stream coordinate frames (as defined in the references
35
+ below), and for transforming positions and velocities to and from coordinate
36
+ systems defined by great circles or poles. These classes behave like the
37
+ built-in astropy coordinates frames (e.g., :class:`~astropy.coordinates.ICRS` or
38
+ :class:`~astropy.coordinates.Galactic`) and can be transformed to and from other
39
+ astropy coordinate frames. For example, to convert a set of
40
+ `~astropy.coordinates.ICRS` (RA, Dec) coordinates to a coordinate system aligned
41
+ with the Sagittarius stream with the `~gala.coordinates.SagittariusLaw10`
42
+ frame::
43
+
44
+ >>> c = coord.ICRS(ra=100.68458*u.degree, dec=41.26917*u.degree)
45
+ >>> sgr = c.transform_to(gc.SagittariusLaw10())
46
+ >>> (sgr.Lambda, sgr.Beta) # doctest: +FLOAT_CMP
47
+ (<Longitude 179.58511053544734 deg>, <Latitude -12.558450192162654 deg>)
48
+
49
+ Or, to transform from `~gala.coordinates.SagittariusLaw10` coordinates to the
50
+ `~astropy.coordinates.Galactic` frame::
51
+
52
+ >>> sgr = gc.SagittariusLaw10(Lambda=156.342*u.degree, Beta=1.1*u.degree)
53
+ >>> c = sgr.transform_to(coord.Galactic())
54
+ >>> (c.l, c.b) # doctest: +FLOAT_CMP
55
+ (<Longitude 182.5922090437946 deg>, <Latitude -9.539692094685893 deg>)
56
+
57
+ These transformations also handle velocities so that proper motion components
58
+ can be transformed between the systems. For example, to transform from
59
+ `~gala.coordinates.GD1Koposov10` proper motions to
60
+ `~astropy.coordinates.Galactic` proper motions::
61
+
62
+ >>> gd1 = gc.GD1Koposov10(phi1=-35*u.degree, phi2=0*u.degree,
63
+ ... pm_phi1_cosphi2=-12.20*u.mas/u.yr,
64
+ ... pm_phi2=-3.10*u.mas/u.yr)
65
+ >>> gd1.transform_to(coord.Galactic()) # doctest: +FLOAT_CMP
66
+ <Galactic Coordinate: (l, b) in deg
67
+ (181.28968151, 54.84972806)
68
+ (pm_l_cosb, pm_b) in mas / yr
69
+ (12.03209393, -3.69847479)>
70
+
71
+ As with the other Astropy coordinate frames, with a full specification of the 3D
72
+ position and velocity, we can transform to a
73
+ `~astropy.coordinates.Galactocentric` frame::
74
+
75
+ >>> gd1 = gc.GD1Koposov10(phi1=-35.00*u.degree, phi2=0.04*u.degree,
76
+ ... distance=7.83*u.kpc,
77
+ ... pm_phi1_cosphi2=-12.20*u.mas/u.yr,
78
+ ... pm_phi2=-3.10*u.mas/u.yr,
79
+ ... radial_velocity=-32*u.km/u.s)
80
+ >>> gd1.transform_to(coord.Galactocentric()) # doctest: +FLOAT_CMP
81
+ <Galactocentric Coordinate (galcen_coord=<ICRS Coordinate: (ra, dec) in deg
82
+ (266.4051, -28.936175)>, galcen_distance=8.122 kpc, galcen_v_sun=(12.9, 245.6, 7.78) km / s, z_sun=20.8 pc, roll=0.0 deg): (x, y, z) in kpc
83
+ (-12.61622659, -0.09870921, 6.43179403)
84
+ (v_x, v_y, v_z) in km / s
85
+ (-71.14675268, -203.01648654, -97.12884319)>
86
+
87
+ For custom great circle coordinate systems, and for more information about the
88
+ stellar stream frames, see :ref:`greatcircle`.
89
+
90
+
91
+ Correcting velocities for solar reflex motion
92
+ ---------------------------------------------
93
+
94
+ The `~gala.coordinates.reflex_correct` function accepts an Astropy
95
+ `~astropy.coordinates.SkyCoord` object with position and velocity information,
96
+ and returns a coordinate object with the solar motion added back in to the
97
+ velocity components. This is useful for computing velocities in a Galactocentric
98
+ reference frame, rather than a solar system barycentric frame.
99
+
100
+ The `~gala.coordinates.reflex_correct` function accepts a coordinate object with
101
+ scalar or array values::
102
+
103
+ >>> c = coord.SkyCoord(ra=[180.323, 1.523]*u.deg,
104
+ ... dec=[-17, 29]*u.deg,
105
+ ... distance=[172, 412]*u.pc,
106
+ ... pm_ra_cosdec=[-11, 3]*u.mas/u.yr,
107
+ ... pm_dec=[4, 8]*u.mas/u.yr,
108
+ ... radial_velocity=[114, -21]*u.km/u.s)
109
+ >>> gc.reflex_correct(c) # doctest: +FLOAT_CMP
110
+ <SkyCoord (ICRS): (ra, dec, distance) in (deg, deg, pc)
111
+ [(180.323, -17., 172.), ( 1.523, 29., 412.)]
112
+ (pm_ra_cosdec, pm_dec, radial_velocity) in (mas / yr, mas / yr, km / s)
113
+ [(139.47001884, 175.45769809, -47.09032586),
114
+ (-61.01738781, 61.51055793, 163.36721898)]>
115
+
116
+ By default, this uses the solar location and velocity from the
117
+ `astropy.coordinates.Galactocentric` frame class. To modify these parameters,
118
+ for example, to change the solar velocity, or the sun's height above the
119
+ Galactic midplane, use the arguments of the `astropy.coordinates.Galactocentric`
120
+ class and pass in an instance of the `astropy.coordinates.Galactocentric`
121
+ frame::
122
+
123
+ >>> vsun = coord.CartesianDifferential([11., 245., 7.]*u.km/u.s)
124
+ >>> gc_frame = coord.Galactocentric(galcen_v_sun=vsun, z_sun=0*u.pc)
125
+ >>> gc.reflex_correct(c, gc_frame) # doctest: +FLOAT_CMP
126
+ <SkyCoord (ICRS): (ra, dec, distance) in (deg, deg, pc)
127
+ [(180.323, -17., 172.), ( 1.523, 29., 412.)]
128
+ (pm_ra_cosdec, pm_dec, radial_velocity) in (mas / yr, mas / yr, km / s)
129
+ [(136.93481249, 175.37627916, -47.6177433 ),
130
+ (-59.96484921, 61.41044742, 163.90707073)]>
131
+
132
+ If you don't have radial velocity information and want to correct the proper
133
+ motions, pass in zeros for the radial velocity (and ignore the output value of
134
+ the radial velocity)::
135
+
136
+ >>> c = coord.SkyCoord(ra=162*u.deg,
137
+ ... dec=-17*u.deg,
138
+ ... distance=172*u.pc,
139
+ ... pm_ra_cosdec=-11*u.mas/u.yr,
140
+ ... pm_dec=4*u.mas/u.yr,
141
+ ... radial_velocity=0*u.km/u.s)
142
+ >>> gc.reflex_correct(c) # doctest: +FLOAT_CMP
143
+ <SkyCoord (ICRS): (ra, dec, distance) in (deg, deg, pc)
144
+ (162., -17., 172.)
145
+ (pm_ra_cosdec, pm_dec, radial_velocity) in (mas / yr, mas / yr, km / s)
146
+ (88.20380175, 163.68500525, -192.48721942)>
147
+
148
+ Similarly, if you don't have proper motion information and want to correct the
149
+ proper motions, pass in zeros for the proper motions (and ignore the output
150
+ values of the proper motions) -- this is sometimes called "v_GSR"::
151
+
152
+ >>> c = coord.SkyCoord(ra=162*u.deg,
153
+ ... dec=-17*u.deg,
154
+ ... distance=172*u.pc,
155
+ ... pm_ra_cosdec=0*u.mas/u.yr,
156
+ ... pm_dec=0*u.mas/u.yr,
157
+ ... radial_velocity=127*u.km/u.s)
158
+ >>> gc.reflex_correct(c) # doctest: +FLOAT_CMP
159
+ <SkyCoord (ICRS): (ra, dec, distance) in (deg, deg, pc)
160
+ (162., -17., 172.)
161
+ (pm_ra_cosdec, pm_dec, radial_velocity) in (mas / yr, mas / yr, km / s)
162
+ (99.20380175, 159.68500525, -65.48721942)>
163
+
164
+
165
+ Transforming a proper motion covariance matrix to a new coordinate frame
166
+ ------------------------------------------------------------------------
167
+
168
+ When working with Gaia or other astrometric data sets, you may need to transform
169
+ the reported covariance matrix between proper motion components into a new
170
+ coordinate system. For example, Gaia data are provided in the
171
+ `~astropy.coordinates.ICRS` (equatorial) coordinate frame, but for Galactic
172
+ science, we often want to instead work in the `~astropy.coordinates.Galactic`
173
+ coordinate system. For this and other transformations that only require a
174
+ rotation (i.e. the origin doesn't change), the astrometric covariance matrix can
175
+ be transformed exactly through a projection of the rotation onto the tangent
176
+ plane at a given location. The details of this procedure are explained in `this
177
+ document from the Gaia data processing team
178
+ <https://gea.esac.esa.int/archive/documentation/GDR2/Data_processing/chap_cu3ast/sec_cu3ast_intro/ssec_cu3ast_intro_tansforms.html>`_,
179
+ and this functionality is implemented in `gala`. Let's first create a coordinate
180
+ object to transform::
181
+
182
+ >>> c = coord.SkyCoord(ra=62*u.deg,
183
+ ... dec=17*u.deg,
184
+ ... pm_ra_cosdec=1*u.mas/u.yr,
185
+ ... pm_dec=3*u.mas/u.yr)
186
+
187
+ and a covariance matrix for the proper motion components, for example, as would
188
+ be constructed from a single row from a Gaia data release source catalog::
189
+
190
+ >>> cov = np.array([[0.53510132, 0.16637034],
191
+ ... [0.16637034, 1.1235292 ]])
192
+
193
+ This matrix specifies the 2D error distribution for the proper motion
194
+ measurement *in the ICRS frame*. To transform this matrix to, e.g., the Galactic
195
+ coordinate system, use the function
196
+ `~gala.coordinates.transform_pm_cov`::
197
+
198
+ >>> gc.transform_pm_cov(c, cov, coord.Galactic()) # doctest: +FLOAT_CMP
199
+ array([[ 0.69450047, -0.309945 ],
200
+ [-0.309945 , 0.96413005]])
201
+
202
+ Note that this also works for all of the great circle or stellar stream
203
+ coordinate frames implemented in `gala`::
204
+
205
+ >>> gc.transform_pm_cov(c, cov, gc.GD1Koposov10()) # doctest: +FLOAT_CMP
206
+ array([[1.10838914, 0.19067958],
207
+ [0.19067958, 0.55024138]])
208
+
209
+ This works for array-valued coordinates as well, so try to avoid looping over
210
+ this function and instead apply it to array-valued coordinate objects.
211
+
212
+
213
+ References
214
+ ----------
215
+
216
+ * `A 2MASS All-Sky View of the Sagittarius Dwarf Galaxy: I. Morphology of the
217
+ Sagittarius Core and Tidal Arms <http://arxiv.org/abs/astro-ph/0304198>`_
218
+ * `The Orbit of the Orphan Stream <http://arxiv.org/abs/1001.0576>`_
219
+ * `Constraining the Milky Way potential with a 6-D phase-space map of the GD-1
220
+ stellar stream <https://arxiv.org/abs/0907.1085>`_
221
+
222
+
223
+ Using gala.coordinates
224
+ ======================
225
+
226
+ More details are provided in the linked pages below:
227
+
228
+ .. toctree::
229
+ :maxdepth: 1
230
+
231
+ greatcircle
232
+
233
+
234
+ .. _gala-coordinates-api:
235
+
236
+ API
237
+ ===
238
+
239
+ .. automodapi:: gala.coordinates
240
+ :no-inheritance-diagram:
241
+ :no-main-docstr:
gala/source/docs/docs.rst ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _gala-docs:
2
+
3
+ =================
4
+ Building the docs
5
+ =================
6
+
7
+ The documentation is built by Sphinx. To start, make sure you install all of the docs dependencies::
8
+
9
+ pip install -e ".[docs]"
10
+
11
+ Then change directory into the ``docs/`` path. You now have to execute the
12
+ tutorials, make animations needed by the documentation, and run the docs build::
13
+
14
+ make exectutorials
15
+ make animations
16
+ make html
gala/source/docs/dynamics/actionangle.rst ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _gala-actionangle:
2
+
3
+ ************************************************
4
+ Transforming to actions, angles, and frequencies
5
+ ************************************************
6
+
7
+ Introduction
8
+ ============
9
+
10
+ Regular orbits permit a (local) transformation to a set of canonical coordinates
11
+ such that the momenta are independent, isolating integrals of motion (the
12
+ actions, :math:`\boldsymbol{J}`) and the conjugate coordinate variables (the
13
+ angles, :math:`\boldsymbol{\theta}`) linearly increase with time. Action-angle
14
+ coordinates are useful for a number of applications because the equations of motion are very simple:
15
+
16
+ .. math::
17
+
18
+ H &= H(\boldsymbol{J})\\
19
+ \dot{\boldsymbol{J}} &= -\frac{\partial H}{\partial \boldsymbol{\theta}} = 0\\
20
+ \dot{\boldsymbol{\theta}} &= \frac{\partial H}{\partial \boldsymbol{J}} = \boldsymbol{\Omega}(\boldsymbol{J}) = {\rm constant}
21
+
22
+ Analytic transformations from phase-space to action-angle coordinates are only
23
+ known for a few simple cases where the gravitational potential is separable or
24
+ has many symmetries. However, astronomical systems can often be approximately axisymmetric or triaxial, or have complex radial profiles that are not captured by these simple gravitational potentials where the transformations are known.
25
+
26
+ Several numerical methods have been developed over recent years to enable
27
+ approximate transformations between ordinary position and velocity to
28
+ action-angle coordinates -- see [sanders16]_ for a summary of these methods.
29
+ In Gala, we have implemented the method described in [sanders14]_ -- later in
30
+ [sanders16]_ named the "O2GF" method -- for computing actions and angles from
31
+ numerically integrated orbits. Gala also provides an interface to the `galpy
32
+ <https://github.com/jobovy/galpy>`_ implementation of the "Staeckel Fudge"
33
+ method, which is much faster but only useful for axisymmetric or spherical
34
+ potentials.
35
+
36
+ The O2GF action solver
37
+ ======================
38
+
39
+ As mentioned above, this method was first introduced in [sanders14]_ and later
40
+ described in [sanders16]_. This method is very general in that it works with any
41
+ numerically-integrated orbital time series. However, it is slower than other
42
+ approximate methods: If your system is spherical or axisymmetric, other methods
43
+ will perform much better. If your system is triaxial, this method is your best
44
+ option. We demonstrate this method below with two qualitatively different
45
+ orbits:
46
+
47
+ * :ref:`tube-axisymmetric`
48
+ * :ref:`tube-triaxial`
49
+
50
+ (see also [binneytremaine]_ and [mcgill90]_ for more context). For the examples
51
+ below, we will use the `~gala.units.galactic` unit system and assume the
52
+ following imports have been executed::
53
+
54
+ >>> import astropy.coordinates as coord
55
+ >>> import astropy.units as u
56
+ >>> import matplotlib.pyplot as plt
57
+ >>> import numpy as np
58
+ >>> import gala.dynamics as gd
59
+ >>> import gala.integrate as gi
60
+ >>> import gala.potential as gp
61
+ >>> from gala.units import galactic
62
+
63
+ For many more options for action calculation, see
64
+ `tact <https://github.com/jls713/tact>`_.
65
+
66
+ .. _tube-axisymmetric:
67
+
68
+ A tube orbit in an axisymmetric potential
69
+ -----------------------------------------
70
+
71
+ For an example of an axisymmetric potential, we use a flattened logarithmic
72
+ potential:
73
+
74
+ .. math::
75
+
76
+ \Phi(x,y,z) = \frac{1}{2}v_{\rm c}^2\ln (x^2 + y^2 + (z/q)^2 + r_h^2)
77
+
78
+ with parameters
79
+
80
+ .. math::
81
+
82
+ v_{\rm c} &= 150~{\rm km}~{\rm s}^{-1}\\
83
+ q &= 0.9\\
84
+ r_h &= 0
85
+
86
+ For the orbit, we use initial conditions
87
+
88
+ .. math::
89
+
90
+ \boldsymbol{r} &= (8, 0, 0)~{\rm kpc}\\
91
+ \boldsymbol{v} &= (75, 150, 50)~{\rm km}~{\rm s}^{-1}
92
+
93
+ We first create a potential and set up our initial conditions::
94
+
95
+ >>> pot = gp.LogarithmicPotential(
96
+ ... v_c=150*u.km/u.s, q1=1., q2=1., q3=0.9, r_h=0,
97
+ ... units=galactic)
98
+ >>> w0 = gd.PhaseSpacePosition(pos=[8, 0, 0.]*u.kpc,
99
+ ... vel=[75, 150, 50.]*u.km/u.s)
100
+
101
+ We will now integrate the orbit and plot it in the meridional plane::
102
+
103
+ >>> w = gp.Hamiltonian(pot).integrate_orbit(w0, dt=0.5, n_steps=10000)
104
+ >>> cyl = w.represent_as('cylindrical')
105
+ >>> fig = cyl.plot(['rho', 'z'], linestyle='-') # doctest: +SKIP
106
+
107
+ .. plot::
108
+ :align: center
109
+ :context: close-figs
110
+ :width: 60%
111
+
112
+ import astropy.coordinates as coord
113
+ import astropy.units as u
114
+ import matplotlib.pyplot as plt
115
+ import numpy as np
116
+ import gala.potential as gp
117
+ import gala.dynamics as gd
118
+ from gala.units import galactic
119
+
120
+ pot = gp.LogarithmicPotential(v_c=150*u.km/u.s, q1=1., q2=1., q3=0.9, r_h=0,
121
+ units=galactic)
122
+ w0 = gd.PhaseSpacePosition(pos=[8, 0, 0.]*u.kpc,
123
+ vel=[75, 150, 50.]*u.km/u.s)
124
+
125
+ w = gp.Hamiltonian(pot).integrate_orbit(w0, dt=0.5, n_steps=10000)
126
+ cyl = w.represent_as('cylindrical')
127
+ cyl.plot(['rho', 'z'], linestyle='-')
128
+
129
+ To solve for the actions in the true potential, we first compute the actions in
130
+ a "toy" potential -- a potential in which we can compute the actions and angles
131
+ analytically. The two simplest potentials for which this is possible are the
132
+ `~gala.potential.potential.IsochronePotential` and
133
+ `~gala.potential.potential.HarmonicOscillatorPotential`. We will use the
134
+ Isochrone potential as our toy potential for tube orbits and the harmonic
135
+ oscillator for box orbits.
136
+
137
+ We start by finding the parameters of the toy potential (Isochrone in this case)
138
+ by minimizing the dispersion in energy for the orbit::
139
+
140
+ >>> toy_potential = gd.fit_isochrone(w)
141
+ >>> toy_potential
142
+ <IsochronePotential: m=1.24e+11, b=4.02 (kpc,Myr,solMass,rad)>
143
+
144
+ The actions and angles in this potential are not the true actions, but will only
145
+ serve as an approximation. This can be seen in the angles: the orbit in the true
146
+ angles would be perfectly straight lines with slope equal to the frequencies.
147
+ Instead, the orbit is wobbly in the toy potential angles::
148
+
149
+ >>> toy_actions,toy_angles,toy_freqs = toy_potential.action_angle(w)
150
+ >>> fig,ax = plt.subplots(1,1,figsize=(5,5))
151
+ >>> ax.plot(toy_angles[0], toy_angles[2], linestyle='none', marker=',') # doctest: +SKIP
152
+ >>> ax.set_xlim(0,2*np.pi) # doctest: +SKIP
153
+ >>> ax.set_ylim(0,2*np.pi) # doctest: +SKIP
154
+ >>> ax.set_xlabel(r"$\theta_1$ [rad]") # doctest: +SKIP
155
+ >>> ax.set_ylabel(r"$\theta_3$ [rad]") # doctest: +SKIP
156
+
157
+ .. plot::
158
+ :align: center
159
+ :context: close-figs
160
+ :width: 60%
161
+
162
+ toy_potential = gd.fit_isochrone(w)
163
+ toy_actions,toy_angles,toy_freqs = toy_potential.action_angle(w)
164
+ fig,ax = plt.subplots(1,1,figsize=(5,5))
165
+ ax.plot(toy_angles[0], toy_angles[2], linestyle='none', marker=',')
166
+ ax.set_xlim(0,2*np.pi)
167
+ ax.set_ylim(0,2*np.pi)
168
+ ax.set_xlabel(r"$\theta_1$ [rad]")
169
+ ax.set_ylabel(r"$\theta_3$ [rad]")
170
+ fig.tight_layout()
171
+
172
+ This can also be seen in the value of the action variables, which are not
173
+ time-independent in the toy potential::
174
+
175
+ >>> fig,ax = plt.subplots(1,1)
176
+ >>> ax.plot(w.t, toy_actions[0], marker='') # doctest: +SKIP
177
+ >>> ax.set_xlabel(r"$t$ [Myr]") # doctest: +SKIP
178
+ >>> ax.set_ylabel(r"$J_1$ [rad]") # doctest: +SKIP
179
+
180
+ .. plot::
181
+ :align: center
182
+ :context: close-figs
183
+ :width: 60%
184
+
185
+ fig,ax = plt.subplots(1,1)
186
+ ax.plot(w.t, toy_actions[0].to(u.km/u.s*u.kpc), marker='')
187
+ ax.set_xlabel(r"$t$ [Myr]")
188
+ ax.set_ylabel(r"$J_1$ [kpc km/s]")
189
+ fig.tight_layout()
190
+
191
+ We can now find approximations to the actions in the true potential. We have to
192
+ choose the maximum integer vector norm, `N_max`, which here we arbitrarily set
193
+ to 8. This will change depending on the convergence of the action correction
194
+ (the properties of the orbit and potential) and the accuracy desired::
195
+
196
+ >>> result = gd.find_actions_o2gf(w, N_max=8, toy_potential=toy_potential) # doctest: +SKIP
197
+ >>> result.keys() # doctest: +SKIP
198
+ dict_keys(['Sn', 'nvecs', 'freqs', 'dSn_dJ', 'angles', 'actions'])
199
+
200
+ The value of the actions, frequencies, and the angles at t=0 are returned in
201
+ the result dictionary::
202
+
203
+ >>> result['actions'] # doctest: +SKIP
204
+ <Quantity [ 0.12472277, 1.22725461, 0.05847431] kpc2 solMass / Myr>
205
+
206
+ To visualize how the actions are computed, we again plot the actions in the
207
+ toy potential and then plot the "corrected" actions -- the approximation to the
208
+ actions computed using this machinery::
209
+
210
+ >>> nvecs = gd.generate_n_vectors(8, dx=1, dy=2, dz=2) # doctest: +SKIP
211
+ >>> act_correction = nvecs.T[...,None] * result['Sn'][None,:,None] * np.cos(nvecs.dot(toy_angles))[None] # doctest: +SKIP
212
+ >>> action_approx = toy_actions - 2*np.sum(act_correction, axis=1)*u.kpc**2/u.Myr # doctest: +SKIP
213
+ >>>
214
+ >>> fig,ax = plt.subplots(1,1) # doctest: +SKIP
215
+ >>> ax.plot(w.t, toy_actions[0].to(u.km/u.s*u.kpc), marker='', label='$J_1$') # doctest: +SKIP
216
+ >>> ax.plot(w.t, action_approx[0].to(u.km/u.s*u.kpc), marker='', label="$J_1'$") # doctest: +SKIP
217
+ >>> ax.set_xlabel(r"$t$ [Myr]") # doctest: +SKIP
218
+ >>> ax.set_ylabel(r"[kpc ${\rm M}_\odot$ km/s]") # doctest: +SKIP
219
+ >>> ax.legend() # doctest: +SKIP
220
+
221
+ .. plot::
222
+ :align: center
223
+ :context: close-figs
224
+ :width: 60%
225
+
226
+ import warnings
227
+ with warnings.catch_warnings(record=True):
228
+ warnings.simplefilter("ignore")
229
+ result = gd.find_actions_o2gf(w, N_max=8, toy_potential=toy_potential)
230
+
231
+ nvecs = gd.generate_n_vectors(8, dx=1, dy=2, dz=2)
232
+ act_correction = nvecs.T[...,None] * result['Sn'][0][None,:,None] * np.cos(nvecs.dot(toy_angles))[None]
233
+ action_approx = toy_actions - 2*np.sum(act_correction, axis=1)*u.kpc**2/u.Myr
234
+ fig,ax = plt.subplots(1,1)
235
+ ax.plot(w.t, toy_actions[0].to(u.km/u.s*u.kpc), marker='', label='$J_1$')
236
+ ax.plot(w.t, action_approx[0].to(u.km/u.s*u.kpc), marker='', label="$J_1'$")
237
+ ax.set_xlabel(r"$t$ [Myr]")
238
+ ax.set_ylabel(r"[kpc ${\rm M}_\odot$ km/s]")
239
+ ax.legend()
240
+
241
+ Above the blue line represents the approximation of the actions in the true
242
+ potential.
243
+
244
+ .. _tube-triaxial:
245
+
246
+ A tube orbit in a triaxial potential
247
+ ------------------------------------
248
+
249
+ The same procedure works for regular orbits in more complex potentials. We
250
+ demonstrate this below by repeating the above in a triaxial potential. We again
251
+ use a logarithmic potential, but with flattening along two dimensions:
252
+
253
+ .. math::
254
+
255
+ \Phi(x,y,z) = \frac{1}{2}v_{\rm c}^2\ln ((x/q_1)^2 + (y/q_2)^2 + (z/q_3)^2)
256
+
257
+ with parameter values:
258
+
259
+ .. math::
260
+
261
+ v_{\rm c} &= 150~{\rm km}~{\rm s}^{-1}\\
262
+ q_1 &= 1\\
263
+ q_2 &= 0.9\\
264
+ q_3 &= 0.8\\
265
+ r_h &= 0
266
+
267
+ and the same initial conditions as above:
268
+
269
+ .. math::
270
+
271
+ \boldsymbol{r} &= (8, 0, 0)~{\rm kpc}\\
272
+ \boldsymbol{v} &= (75, 150, 50)~{\rm km}~{\rm s}^{-1}
273
+
274
+ .. plot::
275
+ :align: center
276
+ :include-source:
277
+ :width: 60%
278
+
279
+ import astropy.coordinates as coord
280
+ import astropy.units as u
281
+ import matplotlib.pyplot as plt
282
+ import numpy as np
283
+ import gala.potential as gp
284
+ import gala.dynamics as gd
285
+ from gala.units import galactic
286
+
287
+ # define potential
288
+ pot = gp.LogarithmicPotential(v_c=150*u.km/u.s, q1=1., q2=0.9, q3=0.8, r_h=0,
289
+ units=galactic)
290
+
291
+ # define initial conditions
292
+ w0 = gd.PhaseSpacePosition(pos=[8, 0, 0.]*u.kpc,
293
+ vel=[75, 150, 50.]*u.km/u.s)
294
+
295
+ # integrate orbit
296
+ w = gp.Hamiltonian(pot).integrate_orbit(w0, dt=0.5, n_steps=10000)
297
+
298
+ # solve for toy potential parameters
299
+ toy_potential = gd.fit_isochrone(w)
300
+
301
+ # compute the actions,angles in the toy potential
302
+ toy_actions,toy_angles,toy_freqs = toy_potential.action_angle(w)
303
+
304
+ # find approximations to the actions in the true potential
305
+ import warnings
306
+ with warnings.catch_warnings(record=True):
307
+ warnings.simplefilter("ignore")
308
+ result = gd.find_actions_o2gf(w, N_max=8, toy_potential=toy_potential)
309
+
310
+ # for visualization, compute the action correction used to transform the
311
+ # toy potential actions to the approximate true potential actions
312
+ nvecs = gd.generate_n_vectors(8, dx=1, dy=2, dz=2)
313
+ act_correction = nvecs.T[...,None] * result['Sn'][0][None,:,None] * np.cos(nvecs.dot(toy_angles))[None]
314
+ action_approx = toy_actions - 2*np.sum(act_correction, axis=1)*u.kpc**2/u.Myr
315
+
316
+ fig,axes = plt.subplots(3,1,figsize=(6,14))
317
+
318
+ for i,ax in enumerate(axes):
319
+ ax.plot(w.t, toy_actions[i].to(u.km/u.s*u.kpc), marker='', label='$J_{}$'.format(i+1))
320
+ ax.plot(w.t, action_approx[i].to(u.km/u.s*u.kpc), marker='', label="$J_{}'$".format(i+1))
321
+ ax.set_ylabel(r"[kpc ${\rm M}_\odot$ km/s]")
322
+ ax.legend(loc='upper left')
323
+
324
+ ax.set_xlabel(r"$t$ [Myr]")
325
+ fig.tight_layout()
326
+
327
+
328
+ Using the Staeckel Fudge in Galpy
329
+ =================================
330
+
331
+ Gala can transform its ``Orbit`` and ``Potential`` objects into `Galpy <https://github.com/jobovy/galpy>`_ ``Orbit`` and ``Potential`` objects, making it possible to easily use the "Staeckel Fudge" [binney12]_ implementation in Galpy. This method, as
332
+ implemented, is only applicable for axisymmetric systems, but is *much* faster
333
+ than the O2GF method for estimating actions, angles, and frequencies from
334
+ phase-space positions. As an example of this functionality, below we will
335
+ compute the vertical frequency as a function of action for a grid of orbits in a
336
+ two-component model for a galactic potential (a disk + halo model).
337
+
338
+ We will start by defining the potential model::
339
+
340
+ >>> halo = gp.NFWPotential.from_M200_c(
341
+ ... M200=1e12*u.Msun, c=15,
342
+ ... units=galactic
343
+ ... )
344
+ >>> disk = gp.MN3ExponentialDiskPotential(
345
+ ... m=8e10*u.Msun, h_R=3.5*u.kpc, h_z=0.4*u.kpc,
346
+ ... units=galactic
347
+ ... )
348
+ >>> pot = halo + disk
349
+
350
+ We next define a grid of orbital initial conditions with close to the circular
351
+ velocity but varying vertical velocities::
352
+
353
+ >>> vcirc = pot.circular_velocity([8, 0, 0])
354
+ >>> vz_grid = np.linspace(0.5, 200, 64) * u.km/u.s
355
+ >>> xyz = np.repeat([[8., 0, 0]], len(vz_grid), axis=0).T * u.kpc
356
+ >>> vxyz = np.repeat([[0, 1.1, 0]], len(vz_grid), axis=0).T * vcirc
357
+ >>> vxyz[2] = vz_grid
358
+ >>> w0 = gd.PhaseSpacePosition(xyz, vxyz)
359
+
360
+ We can now integrate these orbits in the total potential. Note that we can
361
+ specify the integrator using a string name::
362
+
363
+ >>> orbits = pot.integrate_orbit(
364
+ ... w0, dt=1, t1=0, t2=4*u.Gyr,
365
+ ... Integrator='dopri853'
366
+ ... )
367
+ >>> orbits.cylindrical.plot(['rho', 'z'], alpha=0.5, marker=',') # doctest: +SKIP
368
+
369
+ .. plot::
370
+ :align: center
371
+ :width: 60%
372
+ :context: close-figs
373
+
374
+ import astropy.coordinates as coord
375
+ import astropy.units as u
376
+ import matplotlib.pyplot as plt
377
+ import numpy as np
378
+ import gala.potential as gp
379
+ import gala.integrate as gi
380
+ import gala.dynamics as gd
381
+ from gala.units import galactic
382
+
383
+ halo = gp.NFWPotential.from_M200_c(
384
+ M200=1e12*u.Msun, c=15,
385
+ units=galactic
386
+ )
387
+ disk = gp.MN3ExponentialDiskPotential(
388
+ m=8e10*u.Msun, h_R=3.5*u.kpc, h_z=0.4*u.kpc,
389
+ units=galactic
390
+ )
391
+ pot = halo + disk
392
+
393
+ vcirc = pot.circular_velocity([8, 0, 0])
394
+ vz_grid = np.linspace(0.5, 200, 64) * u.km/u.s
395
+ xyz = np.repeat([[8., 0, 0]], len(vz_grid), axis=0).T * u.kpc
396
+ vxyz = np.repeat([[0, 1.1, 0]], len(vz_grid), axis=0).T * vcirc
397
+ vxyz[2] = vz_grid
398
+ w0 = gd.PhaseSpacePosition(xyz, vxyz)
399
+
400
+ orbits = pot.integrate_orbit(
401
+ w0, dt=1, t1=0, t2=4*u.Gyr,
402
+ Integrator='dopri853'
403
+ )
404
+ orbits.cylindrical.plot(['rho', 'z'], alpha=0.5, marker=',')
405
+
406
+
407
+ With the orbits in hand, we can compute the approximate actions, angles, and
408
+ frequencies with the Staeckel Fudge using Galpy (for more information, see the
409
+ `Galpy documentation <https://docs.galpy.org/en/v1.7.2/actionAngle.html>`_):
410
+
411
+ .. doctest-requires:: galpy
412
+
413
+ >>> from gala.dynamics.actionangle import get_staeckel_fudge_delta
414
+ >>> from galpy.actionAngle import actionAngleStaeckel
415
+ >>> galpy_potential = pot.as_interop("galpy")
416
+ >>> J = np.zeros((3, orbits.norbits))
417
+ >>> Omega = np.zeros((3, orbits.norbits))
418
+ >>> for n, orbit in enumerate(orbits.orbit_gen()): # doctest: +SKIP
419
+ ... o = orbit.to_galpy_orbit()
420
+ ... delta = get_staeckel_fudge_delta(pot, orbit)
421
+ ... staeckel = actionAngleStaeckel(pot=galpy_potential, delta=delta)
422
+ ... af = staeckel.actionsFreqs(o)
423
+ ... af = np.mean(np.stack(af), axis=1)
424
+ ... J[:3, n] = af[:3]
425
+ ... Omega[:3, n] = af[3:]
426
+
427
+ Let's visualize the dependence of the vertical action on the value of the
428
+ vertical velocity we used as initial conditions:
429
+
430
+ .. doctest-requires:: galpy
431
+
432
+ >>> plt.plot(w0.v_z, J[2]) # doctest: +SKIP
433
+
434
+ .. plot::
435
+ :align: center
436
+ :width: 60%
437
+ :context: close-figs
438
+
439
+ from gala.dynamics.actionangle import get_staeckel_fudge_delta
440
+ from galpy.actionAngle import actionAngleStaeckel
441
+
442
+ galpy_potential = pot.as_interop("galpy")
443
+ J = np.zeros((3, orbits.norbits))
444
+ Omega = np.zeros((3, orbits.norbits))
445
+ for n, orbit in enumerate(orbits.orbit_gen()):
446
+ o = orbit.to_galpy_orbit()
447
+ delta = get_staeckel_fudge_delta(pot, orbit)
448
+ staeckel = actionAngleStaeckel(pot=galpy_potential, delta=delta)
449
+ af = staeckel.actionsFreqs(o)
450
+ af = np.mean(np.stack(af), axis=1)
451
+
452
+ J[:3, n] = af[:3]
453
+ Omega[:3, n] = af[3:]
454
+
455
+ fig, ax = plt.subplots(figsize=(6, 6), constrained_layout=True)
456
+ ax.plot(w0.v_z, J[2])
457
+ ax.set_xlabel(f"$v_z$ [{w0.v_z.unit:latex_inline}]")
458
+ ax.set_ylabel(rf"$J_z$")
459
+
460
+
461
+ The overall trend looks right, but what is that weird break that occurs around
462
+ :math:`v_z` ~ 120 km/s? Let's visualize orbits with initial conditions just next to and
463
+ within this region:
464
+
465
+ .. doctest-requires:: galpy
466
+
467
+ >>> i1 = np.abs(w0.v_z.value - 120).argmin()
468
+ >>> i2 = np.abs(w0.v_z.value - 100).argmin()
469
+ >>> orbits[:, i1].cylindrical.plot(['rho', 'z'], alpha=0.5, marker=',') # doctest: +SKIP
470
+ >>> orbits[:, i2].cylindrical.plot(['rho', 'z'], alpha=0.5, marker=',') # doctest: +SKIP
471
+
472
+ .. plot::
473
+ :align: center
474
+ :width: 90%
475
+ :context: close-figs
476
+
477
+ fig, axes = plt.subplots(1, 2, figsize=(10, 5),
478
+ sharex=True, sharey=True,
479
+ constrained_layout=True)
480
+
481
+ i1 = np.abs(w0.v_z.value - 120).argmin()
482
+ i2 = np.abs(w0.v_z.value - 100).argmin()
483
+ orbits[:, i1].cylindrical.plot(['rho', 'z'], alpha=0.5, marker=',', axes=[axes[0]]);
484
+ orbits[:, i2].cylindrical.plot(['rho', 'z'], alpha=0.5, marker=',', axes=[axes[1]]);
485
+
486
+ Aha! This region is special: it is a resonance in the potential. Orbits in this region
487
+ of phase-space have qualitatively different behavior than those outside of this region
488
+ because they are trapped by the resonance. For these orbits, where strong potential
489
+ resonances occur, the Staeckel Fudge approximation will return incorrect and potentially
490
+ misleading action, angle, and frequency values.
491
+
492
+
493
+ References
494
+ ==========
495
+
496
+ .. [binney12] Binney (2012) `Actions for axisymmetric potentials
497
+ <https://ui.adsabs.harvard.edu/abs/2012MNRAS.426.1324B/abstract>`_
498
+ .. [sanders14] Sanders & Binney (2014) `Actions, angles and frequencies for numerically integrated orbits <http://arxiv.org/abs/1401.3600>`_
499
+ .. [sanders16] Sanders & Binney (2016) `A review of action estimation methods for galactic dynamics <https://ui.adsabs.harvard.edu/abs/2016MNRAS.457.2107S/abstract>`_
500
+ .. [binneytremaine] Binney & Tremaine (2008) `Galactic Dynamics <http://press.princeton.edu/titles/8697.html>`_
501
+ .. [mcgill90] McGill & Binney (1990) `Torus construction in general gravitational potentials <http://articles.adsabs.harvard.edu/cgi-bin/nph-iarticle_query?1990MNRAS.244..634M&amp;data_type=PDF_HIGH&amp;whole_paper=YES&amp;type=PRINTER&amp;filetype=.pdf>`_
gala/source/docs/dynamics/index.rst ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. include:: references.txt
2
+
3
+ .. _gala-dynamics:
4
+
5
+ ********************************
6
+ Dynamics (`gala.dynamics`)
7
+ ********************************
8
+
9
+ For the examples below the following imports have already been executed::
10
+
11
+ >>> import astropy.units as u
12
+ >>> import numpy as np
13
+ >>> import gala.potential as gp
14
+ >>> import gala.dynamics as gd
15
+ >>> from gala.units import galactic
16
+
17
+ Introduction
18
+ ============
19
+
20
+ This subpackage contains functions and classes useful for gravitational
21
+ dynamics. There are utilities for transforming orbits in phase-space to
22
+ action-angle coordinates, tools for visualizing and computing dynamical
23
+ quantities from orbits, tools to generate mock stellar streams, and tools useful
24
+ for nonlinear dynamics such as Lyapunov exponent estimation.
25
+
26
+ The fundamental objects used by many of the functions and utilities in this and
27
+ other subpackages are the |psp| and |orb| classes.
28
+
29
+ Getting started: Working with orbits
30
+ ====================================
31
+
32
+ We'll demonstrate the |psp| and |orb| objects by first integrating an orbit::
33
+
34
+ >>> pot = gp.MiyamotoNagaiPotential(
35
+ ... m=2.5e11 * u.Msun, a=6.5 * u.kpc, b=0.26 * u.kpc, units=galactic
36
+ ... )
37
+ >>> w0 = gd.PhaseSpacePosition(
38
+ ... pos=[11.0, 0.0, 0.2] * u.kpc, vel=[0.0, 200, 100] * u.km / u.s
39
+ ... )
40
+ >>> orbit = gp.Hamiltonian(pot).integrate_orbit(w0, dt=1.0, n_steps=1000)
41
+
42
+ This numerically integrates an orbit from the specified initial conditions,
43
+ ``w0``, and returns an |orb| object. By default, this uses the Leapfrog
44
+ integrator, but you can specify a different integrator using the ``Integrator``
45
+ keyword argument. For example, to use a higher-order adaptive Runge-Kutta
46
+ method::
47
+
48
+ >>> orbit = gp.Hamiltonian(pot).integrate_orbit(
49
+ ... w0, dt=1.0, n_steps=1000, Integrator='dopri853'
50
+ ... )
51
+
52
+ Valid integrator names include ``'leapfrog'``, ``'dopri853'``, ``'ruth4'``, and
53
+ ``'rk5'``. You can also pass an integrator class directly (see
54
+ :ref:`gala-integrate` for more information).
55
+
56
+ By default, the position and velocity are
57
+ assumed to be Cartesian coordinates but other coordinate systems are supported
58
+ (see the :ref:`orbits-in-detail` and :ref:`nd-representations` pages for more
59
+ information).
60
+
61
+ The |orb| object that is returned contains many useful methods, and can be
62
+ passed to many of the analysis functions implemented in Gala. For example, we
63
+ can easily visualize the orbit by plotting the time series in all Cartesian
64
+ projections using the :meth:`~gala.dynamics.Orbit.plot` method::
65
+
66
+ >>> fig = orbit.plot()
67
+
68
+ .. plot::
69
+ :align: center
70
+
71
+ import astropy.units as u
72
+ import gala.potential as gp
73
+ import gala.dynamics as gd
74
+ from gala.units import galactic
75
+ pot = gp.MiyamotoNagaiPotential(m=2.5e11, a=6.5, b=0.26, units=galactic)
76
+ w0 = gd.PhaseSpacePosition(
77
+ pos=[11.0, 0.0, 0.2] * u.kpc, vel=[0.0, 200, 100] * u.km / u.s
78
+ )
79
+ orbit = gp.Hamiltonian(pot).integrate_orbit(w0, dt=1.0, n_steps=1000)
80
+ fig = orbit.plot()
81
+
82
+ We can also visualize the orbit in transformed coordinates, for example,
83
+ cylindrical radius :math:`\rho` and :math:`z`::
84
+
85
+ >>> fig = orbit.represent_as("cylindrical").plot(["rho", "z"])
86
+
87
+ .. plot::
88
+ :align: center
89
+ :width: 60%
90
+
91
+ import astropy.units as u
92
+ import gala.potential as gp
93
+ import gala.dynamics as gd
94
+ from gala.units import galactic
95
+ pot = gp.MiyamotoNagaiPotential(m=2.5e11, a=6.5, b=0.26, units=galactic)
96
+ w0 = gd.PhaseSpacePosition(
97
+ pos=[11.0, 0.0, 0.2] * u.kpc, vel=[0.0, 200, 100] * u.km / u.s
98
+ )
99
+ orbit = gp.Hamiltonian(pot).integrate_orbit(w0, dt=1.0, n_steps=1000)
100
+ _ = orbit.represent_as("cylindrical").plot(["rho", "z"])
101
+
102
+ The |orb| object also enables computing dynamical quantities such as
103
+ energy or angular momentum::
104
+
105
+ >>> E = orbit.energy()
106
+ >>> E[0] # doctest: +SKIP
107
+ <Quantity −0.060740198 kpc2 / Myr2>
108
+
109
+ Let's check how well the integrator conserves energy and the ``z`` component of
110
+ angular momentum::
111
+
112
+ >>> Lz = orbit.angular_momentum()[2]
113
+ >>> np.std(E), np.std(Lz) # doctest: +FLOAT_CMP
114
+ (<Quantity 4.654233175716351e-06 kpc2 / Myr2>,
115
+ <Quantity 9.675900603446092e-16 kpc2 / Myr>)
116
+
117
+ We can access the position and velocity components of the orbit separately using
118
+ attributes that map to the underlying `~astropy.coordinates.BaseRepresentation`
119
+ and `~astropy.coordinates.BaseDifferential` subclass instances that store the
120
+ position and velocity data. The attribute names depend on the representation.
121
+ For example, for a Cartesian representation, the position components are ``["x",
122
+ "y", "z"]`` and the velocity components are ``["v_x", "v_y", "v_z"]``. With a
123
+ |orb| or |psp| instance, you can check the valid compnent names using the
124
+ attributes ``.pos_components`` and ``.vel_components``::
125
+
126
+ >>> orbit.pos_components.keys() # doctest: +SKIP
127
+ odict_keys(["x", "y", "z"])
128
+ >>> orbit.vel_components.keys() # doctest: +SKIP
129
+ odict_keys(["v_x", "v_y", "v_z"])
130
+
131
+ Meaning, we can access these components by doing, e.g.::
132
+
133
+ >>> orbit.v_x # doctest: +FLOAT_CMP
134
+ <Quantity [ 0. , -0.00567589, -0.01129934, ..., 0.18751756,
135
+ 0.18286687, 0.17812762] kpc / Myr>
136
+
137
+ For a Cylindrical representation, these are instead::
138
+
139
+ >>> cyl_orbit = orbit.represent_as("cylindrical")
140
+ >>> cyl_orbit.pos_components.keys() # doctest: +SKIP
141
+ odict_keys(["rho", "phi", "z"])
142
+ >>> cyl_orbit.vel_components.keys() # doctest: +SKIP
143
+ odict_keys(["v_rho", "pm_phi", "v_z"])
144
+ >>> cyl_orbit.v_rho # doctest: +FLOAT_CMP
145
+ <Quantity [ 0. , -0.00187214, -0.00369183, ..., 0.01699321,
146
+ 0.01930216, 0.02159477] kpc / Myr>
147
+
148
+ Continue to the :ref:`orbits-in-detail` page for more information.
149
+
150
+ Using gala.dynamics
151
+ ===================
152
+
153
+ More details are provided in the linked pages below:
154
+
155
+ .. toctree::
156
+ :maxdepth: 2
157
+
158
+ orbits-in-detail
159
+ nd-representations
160
+ actionangle
161
+ mockstreams
162
+ nonlinear
163
+ nbody
164
+
165
+
166
+ API
167
+ ===
168
+
169
+ .. automodapi:: gala.dynamics
170
+ :include: PhaseSpacePosition
171
+ :include: Orbit
172
+ :no-inheritance-diagram:
gala/source/docs/dynamics/mockstreams.rst ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _gala-mockstreams:
2
+
3
+ *******************************
4
+ Generating mock stellar streams
5
+ *******************************
6
+
7
+ Introduction
8
+ ============
9
+
10
+ This module contains functions for generating mock stellar streams using a
11
+ variety of methods that approximate the formation of streams in N-body
12
+ simulations. Mock streams are generated by specifying time-stepping and release
13
+ time information (i.e., when should stream particles be generated), and by
14
+ specifying the stream distribution function (DF) to use to generate initial
15
+ conditions for the stream particles. The former is customizable, and a number of
16
+ popular stream DFs are implemented.
17
+
18
+ Some imports needed for the code below::
19
+
20
+ >>> import astropy.units as u
21
+ >>> import numpy as np
22
+ >>> import gala.potential as gp
23
+ >>> import gala.dynamics as gd
24
+ >>> from gala.dynamics import mockstream as ms
25
+ >>> from gala.units import galactic
26
+
27
+ We will also set the default Astropy Galactocentric frame parameters to the
28
+ values adopted in Astropy v4.0:
29
+
30
+ >>> import astropy.coordinates as coord
31
+ >>> _ = coord.galactocentric_frame_defaults.set('v4.0')
32
+
33
+ Getting started
34
+ ===============
35
+
36
+ All mock stream generation done using the built-in gravitational potential
37
+ models implemented in `gala.potential`, so we must first specify a gravitational
38
+ potential to integrate orbits in. For the examples below, we will use a
39
+ spherical NFW potential with a circular velocity at the scale radius of 220
40
+ km/s, and a scale radius of 15 kpc::
41
+
42
+ >>> pot = gp.NFWPotential.from_circular_velocity(v_c=220*u.km/u.s,
43
+ ... r_s=15*u.kpc,
44
+ ... units=galactic)
45
+
46
+ The mock stream generation supports any of the reference frames implemented in
47
+ ``gala`` (e.g., non-static / rotating reference frames), so we must create a
48
+ `~gala.potential.hamiltonian.Hamiltonian` object to use when generating streams.
49
+ By default, this will use a static reference frame::
50
+
51
+ >>> H = gp.Hamiltonian(pot)
52
+
53
+ Next, we will create initial conditions for the progenitor system. In this case,
54
+ we will generate the mock stream starting from this position going forward in
55
+ time. However, this is customizable: if you instead have the *final* position of
56
+ the progenitor system, there is a convenient way of doing this described below
57
+ (see :ref:`gala-mockstream-final-conditions`). Let's specify a position and
58
+ velocity that we think will produce a mildly eccentric orbit in the x-y plane of
59
+ our coordinate system::
60
+
61
+ >>> prog_w0 = gd.PhaseSpacePosition(pos=[10, 0, 0.] * u.kpc,
62
+ ... vel=[0, 170, 0.] * u.km/u.s)
63
+
64
+ We now have to specify the method for generating stream particles, i.e., the
65
+ stream distribution function (DF). For this example, we will use the method
66
+ implemented in [fardal15]_, incuded in ``gala`` as
67
+ `~gala.dynamics.mockstream.FardalStreamDF`. Other methods of note are
68
+ `~gala.dynamics.mockstream.StreaklineStreamDF` from [kuepper12]_,
69
+ `~gala.dynamics.mockstream.LagrangeCloudStreamDF` based on [gibbons14]_, and
70
+ `~gala.dynamics.mockstream.ChenStreamDF` based on [chen24]_. Each of
71
+ the ``StreamDF`` classes take a few common arguments, such as ``lead`` and
72
+ ``trail``, which are boolean arguments that control whether to generate both
73
+ leading and trailing tails, or just one or the other. By default, both are set
74
+ to True (i.e., both leading and trailing tails are generated by default). Some
75
+ other ``StreamDF`` classes may require other parameters. Let's create a
76
+ `~gala.dynamics.mockstream.ChenStreamDF` instance and accept the default
77
+ argument values. We will also need to specify the progenitor mass, which is
78
+ passed in to any ``StreamDF`` and is used to scale the particle release
79
+ distribution::
80
+
81
+ >>> df = ms.ChenStreamDF()
82
+ >>> prog_mass = 2.5E4 * u.Msun
83
+
84
+ .. warning::
85
+
86
+ The parameter values of the FardalStreamDF have been updated (fixed) in v1.9 to
87
+ match the parameter values in the final published version of [fardal15]_. For now,
88
+ this class uses the Gala modified parameter values that have been adopted over the
89
+ last several years in Gala. In the future, the default behavior of this class will
90
+ use the [fardal15]_ parameter values instead, breaking backwards compatibility for
91
+ mock stream simulations. To use the [fardal15]_ parameters now, set
92
+ ``gala_modified=False``. To continue to use the Gala modified parameter values, set
93
+ ``gala_modified=True``.
94
+
95
+ The final step before actually generating the stream is to create a
96
+ `~gala.dynamics.mockstream.MockStreamGenerator` instance, which we will use to
97
+ actually generate the stream. This takes the ``StreamDF`` and the external
98
+ potential (Hamiltonian) as arguments::
99
+
100
+ >>> gen = ms.MockStreamGenerator(df, H)
101
+
102
+ We are now ready to run the generator and create a mock stream. To do this, we use the `~gala.dynamics.mockstream.MockStreamGenerator.run()` method. This accepts the progenitor orbit initial conditions (we defined above as ``prog_w0``), the progenitor mass (we defined as ``prog_mass``), and time-stepping information. We will integrate the progenitor orbit for 1000 steps with a timestep of 1 Myr::
103
+
104
+ >>> stream, prog = gen.run(prog_w0, prog_mass,
105
+ ... dt=1 * u.Myr, n_steps=1000)
106
+
107
+ Let's plot the stream::
108
+
109
+ >>> stream.plot(['x', 'y']) # doctest: +SKIP
110
+
111
+ .. plot::
112
+ :align: center
113
+ :context: close-figs
114
+ :width: 60%
115
+
116
+ import astropy.units as u
117
+ import numpy as np
118
+ import gala.potential as gp
119
+ import gala.dynamics as gd
120
+ from gala.dynamics import mockstream as ms
121
+ from gala.units import galactic
122
+
123
+ pot = gp.NFWPotential.from_circular_velocity(v_c=220*u.km/u.s,
124
+ r_s=15*u.kpc,
125
+ units=galactic)
126
+ H = gp.Hamiltonian(pot)
127
+ prog_w0 = gd.PhaseSpacePosition(pos=[10, 0, 0.] * u.kpc,
128
+ vel=[0, 170, 0.] * u.km/u.s)
129
+
130
+ df = ms.ChenStreamDF()
131
+ prog_mass = 2.5E4 * u.Msun
132
+
133
+ gen = ms.MockStreamGenerator(df, H)
134
+
135
+ stream, prog = gen.run(prog_w0, prog_mass,
136
+ dt=1 * u.Myr, n_steps=1000)
137
+
138
+ stream.plot(['x', 'y'], marker='o', s=4, color='k', alpha=0.1, linewidth=0)
139
+
140
+
141
+ By default, two stream particles are generated at every timestep in the
142
+ integration of the progenitor orbit (specified above by the timestep ``dt`` and
143
+ number of steps ``n_steps``). We can control the frequency of releasing
144
+ particles, and the number of particles released using the ``release_every`` and
145
+ ``n_particles`` arguments. For example, setting ``release_every=8`` and
146
+ ``n_particles=2`` will instead release 4 particles (2 for each tail) every 8th
147
+ timestep.
148
+
149
+
150
+ Self-gravity of the progenitor
151
+ ==============================
152
+
153
+ Also by default, the progenitor system is assumed to be massless, and the stream
154
+ particles are treated as test particles in the specified external potential or
155
+ Hamiltonian. It is possible to include a potential object for the progenitor
156
+ system to account for the self-gravity of the progenitor as stream star
157
+ particles are released. We can use any of the ``gala.potential`` potential
158
+ objects to represent the progenitor system, but here we will use a simple
159
+ `~gala.potential.potential.PlummerPotential`. We pass this in to the
160
+ `~gala.dynamics.mockstream.MockStreamGenerator` - let's see what the stream
161
+ looks like when generated including self-gravity::
162
+
163
+ >>> prog_pot = gp.PlummerPotential(m=prog_mass, b=4*u.pc, units=galactic)
164
+ >>> gen2 = ms.MockStreamGenerator(df, H, progenitor_potential=prog_pot)
165
+ >>> stream2, prog = gen2.run(prog_w0, prog_mass,
166
+ ... dt=1 * u.Myr, n_steps=1000)
167
+ >>> stream2.plot(['x', 'y']) # doctest: +SKIP
168
+
169
+ .. plot::
170
+ :align: center
171
+ :context: close-figs
172
+ :width: 60%
173
+
174
+ prog_pot = gp.PlummerPotential(m=prog_mass, b=2*u.pc, units=galactic)
175
+ gen2 = ms.MockStreamGenerator(df, H, progenitor_potential=prog_pot)
176
+ stream2, prog = gen2.run(prog_w0, prog_mass,
177
+ dt=1 * u.Myr, n_steps=1000)
178
+ stream2.plot(['x', 'y'], marker='o', s=4,
179
+ color='k', alpha=0.1, linewidth=0)
180
+
181
+
182
+ Integration methods and options
183
+ ================================
184
+
185
+ Choosing an integrator
186
+ -----------------------
187
+
188
+ By default, mock stream generation uses the DOPRI853 integrator, which is an
189
+ 8th-order adaptive Runge-Kutta method. This integrator is robust and
190
+ automatically adjusts the timestep to maintain a specified error tolerance.
191
+ However, ``gala`` also supports using the Leapfrog integrator, which is a
192
+ symplectic integrator that uses a fixed timestep and conserves energy better
193
+ over long integrations.
194
+
195
+ To use the Leapfrog integrator instead of DOPRI853, pass the ``Integrator``
196
+ argument to the `~gala.dynamics.mockstream.MockStreamGenerator.run()` method.
197
+ You can pass the integrator class directly::
198
+
199
+ >>> import gala.integrate as gi
200
+ >>> stream_lf, prog = gen.run(prog_w0, prog_mass,
201
+ ... dt=1 * u.Myr, n_steps=1000,
202
+ ... Integrator=gi.LeapfrogIntegrator)
203
+
204
+ or more conveniently, use a string name::
205
+
206
+ >>> stream_lf, prog = gen.run(prog_w0, prog_mass,
207
+ ... dt=1 * u.Myr, n_steps=1000,
208
+ ... Integrator='leapfrog')
209
+
210
+ The Leapfrog integrator is particularly useful for long-term integrations where energy conservation is critical, situations where a fixed timestep is faster or acceptable, or
211
+ when symplectic integration properties are important (e.g., preserving phase space volume).
212
+
213
+ The DOPRI853 integrator (the default) is better suited for N-body integrations or cases with time-varying potentials.
214
+
215
+ Customizing integrator tolerances
216
+ ----------------------------------
217
+
218
+ For the DOPRI853 integrator, you can customize the absolute and relative error
219
+ tolerances using the ``Integrator_kwargs`` argument. This can be useful for
220
+ balancing accuracy and computational speed::
221
+
222
+ >>> # Higher accuracy (slower)
223
+ >>> stream_hi, prog = gen.run(prog_w0, prog_mass,
224
+ ... dt=1 * u.Myr, n_steps=1000,
225
+ ... Integrator_kwargs={'atol': 1e-12, 'rtol': 1e-12})
226
+
227
+ >>> # Lower accuracy (faster)
228
+ >>> stream_lo, prog = gen.run(prog_w0, prog_mass,
229
+ ... dt=1 * u.Myr, n_steps=1000,
230
+ ... Integrator_kwargs={'atol': 1e-8, 'rtol': 1e-8})
231
+
232
+ The default values are ``atol=1e-10`` and ``rtol=1e-10``, which provide a good
233
+ balance between accuracy and speed for most applications. Other DOPRI853 options
234
+ include ``nmax`` (maximum number of steps) and ``dt_max`` (maximum timestep).
235
+
236
+ Note that the Leapfrog integrator uses a fixed timestep (specified by ``dt``),
237
+ so tolerance parameters do not apply.
238
+
239
+
240
+ .. _gala-mockstream-final-conditions:
241
+
242
+ Generating a stream from the present-day progenitor location
243
+ ============================================================
244
+
245
+ In the examples above, we pass in initial conditions for the progenitor and
246
+ generate the mock stream going forward in time. However, we often may want to
247
+ generate a stream such that the final progenitor location ends up at some
248
+ specified phase-space position. By convention, when a negative timestep is
249
+ passed in to `~gala.dynamics.mockstream.MockStreamGenerator.run()`, this is
250
+ interpreted to mean that the input progenitor phase-space position should be the
251
+ *final* position. Internally, this position is integrated backwards to the
252
+ earliest time, then a stream is generated forward from the past time. This is particularly useful when trying to reproduce observed streams, such as the Pal 5 stream::
253
+
254
+ >>> import astropy.coordinates as coord
255
+ >>> pal5_c = coord.SkyCoord(ra=229.018*u.degree, dec=-0.124*u.degree,
256
+ ... distance=22.9*u.kpc,
257
+ ... pm_ra_cosdec=-2.296*u.mas/u.yr,
258
+ ... pm_dec=-2.257*u.mas/u.yr,
259
+ ... radial_velocity=-58.7*u.km/u.s)
260
+ >>> rep = pal5_c.transform_to(coord.Galactocentric()).data
261
+ >>> pal5_w0 = gd.PhaseSpacePosition(rep)
262
+ >>> pal5_mass = 2.5e4 * u.Msun
263
+ >>> pal5_pot = gp.PlummerPotential(m=pal5_mass, b=4*u.pc, units=galactic)
264
+ >>> mw = gp.MilkyWayPotential(version="latest")
265
+ >>> gen_pal5 = ms.MockStreamGenerator(df, mw, progenitor_potential=pal5_pot)
266
+ >>> pal5_stream, _ = gen_pal5.run(pal5_w0, pal5_mass,
267
+ ... dt=-1 * u.Myr, n_steps=4000)
268
+ >>> pal5_stream_c = pal5_stream.to_coord_frame(coord.ICRS())
269
+
270
+ .. plot::
271
+ :align: center
272
+ :context: close-figs
273
+
274
+ import astropy.coordinates as coord
275
+ coord.galactocentric_frame_defaults.set('v4.0')
276
+ pal5_c = coord.SkyCoord(ra=229.018*u.degree, dec=-0.124*u.degree,
277
+ distance=22.9*u.kpc,
278
+ pm_ra_cosdec=-2.296*u.mas/u.yr,
279
+ pm_dec=-2.257*u.mas/u.yr,
280
+ radial_velocity=-58.7*u.km/u.s)
281
+ rep = pal5_c.transform_to(coord.Galactocentric()).data
282
+ pal5_w0 = gd.PhaseSpacePosition(rep)
283
+ pal5_mass = 2.5e4 * u.Msun
284
+ pal5_pot = gp.PlummerPotential(m=pal5_mass, b=4*u.pc, units=galactic)
285
+ mw = gp.MilkyWayPotential(version="latest")
286
+ gen_pal5 = ms.MockStreamGenerator(df, mw, progenitor_potential=pal5_pot)
287
+ pal5_stream, _ = gen_pal5.run(pal5_w0, pal5_mass,
288
+ dt=-1 * u.Myr, n_steps=4000)
289
+ pal5_stream_c = pal5_stream.to_coord_frame(coord.ICRS())
290
+
291
+ fig, ax = plt.subplots(1, 1, figsize=(6, 4))
292
+ ax.scatter(pal5_stream_c.ra.degree, pal5_stream_c.dec.degree,
293
+ marker='o', s=4, color='k', lw=0, alpha=0.1)
294
+ ax.set_xlim(250, 215)
295
+ ax.set_ylim(-10, 10)
296
+ ax.set_xlabel('RA [deg]')
297
+ ax.set_ylabel('Dec [deg]')
298
+ fig.tight_layout()
299
+
300
+
301
+ References
302
+ ==========
303
+
304
+ .. [fardal15] `Fardal, Huang, Weinberg (2015) <http://arxiv.org/abs/1410.1861>`_
305
+ .. [kuepper12] `Küpper, Lane, Heggie (2012) <http://arxiv.org/abs/1111.5013>`_
306
+ .. [gibbons14] `Gibbons et al. (2014) <https://arxiv.org/abs/1406.2243>`_
307
+ .. [chen24] `Chen et al. (2024) <https://arxiv.org/abs/2408.01496>`_
308
+
309
+ API
310
+ ---
311
+ .. automodapi:: gala.dynamics.mockstream
312
+ :no-heading:
313
+ :headings: ^^
gala/source/docs/dynamics/nbody.rst ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _gala-nbody:
2
+
3
+ ******************************
4
+ N-body (`gala.dynamics.nbody`)
5
+ ******************************
6
+
7
+ Introduction
8
+ ============
9
+
10
+ With the `~gala.potential.hamiltonian.Hamiltonian` and potential classes
11
+ (:ref:`potential`), Gala contains functionality for integrating test particle
12
+ orbits in background gravitational fields. To supplement this, Gala also now
13
+ contains some limited functionality for performing N-body orbit integrations
14
+ through direct N-body force calculations between particles. With the
15
+ `gala.dynamics.nbody` subpackage, gravitational fields (i.e., any potential
16
+ class from :mod:`gala.potential`) can be sourced by particles that interact, and
17
+ optionally feel a background/external potential. To use this functionality, the
18
+ core class is `~gala.dynamics.nbody.DirectNBody`. Below, we'll go through a few
19
+ examples of using this class to perform orbit integrations
20
+
21
+ For the examples below the following imports have already been executed::
22
+
23
+ >>> import astropy.units as u
24
+ >>> import numpy as np
25
+ >>> import gala.potential as gp
26
+ >>> import gala.dynamics as gd
27
+ >>> from gala.dynamics.nbody import DirectNBody
28
+ >>> from gala.units import galactic, UnitSystem
29
+
30
+
31
+ Getting started
32
+ ===============
33
+
34
+ The `~gala.dynamics.nbody.DirectNBody`, at minimum, must be instantiated with a
35
+ set of particle orbital initial conditions along with a specification of the
36
+ gravitational fields sourced by each particle --- that is, the number of initial
37
+ conditions must match the input list of gravitational potential objects that
38
+ specify the particle mass distributions. Other optional arguments to
39
+ `~gala.dynamics.nbody.DirectNBody` allow you to set the unit system (i.e., to
40
+ improve numerical precision when time-stepping the orbit integration), or to
41
+ specify a background gravitational potential. Let's now go through a few
42
+ examples of using this class in practice.
43
+
44
+
45
+ Example: Mixed test particle and massive particle orbit integration
46
+ ===================================================================
47
+
48
+ Like with `~gala.potential.hamiltonian.Hamiltonian` orbit integration, orbital
49
+ initial conditions are passed in to `~gala.dynamics.nbody.DirectNBody` by
50
+ passing in a single `~gala.dynamics.PhaseSpacePosition` object. Let's create two
51
+ initial conditions by specifying the position and velocity of two particles,
52
+ then combine them into a single `~gala.dynamics.PhaseSpacePosition` object::
53
+
54
+ >>> w0_1 = gd.PhaseSpacePosition(pos=[0, 0, 0] * u.pc,
55
+ ... vel=[0, 1.5, 0] * u.km/u.s)
56
+ >>> w0_2 = gd.PhaseSpacePosition(pos=w0_1.xyz + [100., 0, 0] * u.pc,
57
+ ... vel=w0_1.v_xyz + [0, 5, 0] * u.km/u.s)
58
+ >>> w0 = gd.combine((w0_1, w0_2))
59
+ >>> w0.shape
60
+ (2,)
61
+
62
+ We'll then treat particle 1 as a massive object by sourcing a
63
+ `~gala.potential.potential.HernquistPotential` at the location of the particle,
64
+ and particle 2 as a test particle: To treat some particles as test particles,
65
+ you can pass ``None`` or a `~gala.potential.potential.NullPotential` instance
66
+ for the corresponding particle potential::
67
+
68
+ >>> pot1 = gp.HernquistPotential(m=1e7*u.Msun, c=0.5*u.kpc, units=galactic)
69
+ >>> particle_pot = [pot1, None]
70
+
71
+ Let's now create an N-body instance and try integrating the orbits of the two
72
+ particles. Here, there is no external potential, so particle 1 (the massive
73
+ particle) will move off in a straight line. We've created the initial conditions
74
+ for particle 2 so that it will remain bound to the potential sourced by particle
75
+ 1, and so will orbit it as it moves. Let's create the object and integrate the
76
+ orbits::
77
+
78
+ >>> nbody = DirectNBody(w0, particle_pot)
79
+ >>> orbits = nbody.integrate_orbit(dt=1e-2*u.Myr, t1=0, t2=1*u.Gyr)
80
+ >>> fig, ax = plt.subplots(1, 1, figsize=(5, 5)) # doctest: +SKIP
81
+ >>> _ = orbits[:, 0].plot(['x', 'y'], axes=[ax]) # doctest: +SKIP
82
+ >>> _ = orbits[:, 1].plot(['x', 'y'], axes=[ax]) # doctest: +SKIP
83
+
84
+ .. plot::
85
+ :align: center
86
+ :context: close-figs
87
+ :width: 60%
88
+
89
+ import astropy.units as u
90
+ import numpy as np
91
+ import gala.potential as gp
92
+ import gala.dynamics as gd
93
+ from gala.dynamics.nbody import DirectNBody
94
+ from gala.units import galactic, UnitSystem
95
+ import matplotlib.pyplot as plt
96
+
97
+ w0_1 = gd.PhaseSpacePosition(pos=[0, 0, 0] * u.pc,
98
+ vel=[0, 1.5, 0] * u.km/u.s)
99
+ w0_2 = gd.PhaseSpacePosition(pos=w0_1.xyz + [100., 0, 0] * u.pc,
100
+ vel=w0_1.v_xyz + [0, 5, 0] * u.km/u.s)
101
+ w0 = gd.combine((w0_1, w0_2))
102
+
103
+ pot1 = gp.HernquistPotential(m=1e7*u.Msun, c=0.5*u.kpc, units=galactic)
104
+ particle_pot = [pot1, None]
105
+
106
+ nbody = DirectNBody(w0, particle_pot)
107
+ orbits = nbody.integrate_orbit(dt=1e-2*u.Myr, t1=0, t2=1*u.Gyr)
108
+ fig, ax = plt.subplots(1, 1, figsize=(5, 5))
109
+ _ = orbits[:, 0].plot(['x', 'y'], axes=[ax])
110
+ _ = orbits[:, 1].plot(['x', 'y'], axes=[ax])
111
+ fig.tight_layout()
112
+
113
+ Example: N-body integration with a background potential
114
+ =======================================================
115
+
116
+ With `~gala.dynamics.nbody.DirectNBody`, we can also specify a background or
117
+ external potential to integrate all orbits in. To do this, you can optionally
118
+ pass in an external potential as a potential object to
119
+ `~gala.dynamics.nbody.DirectNBody`. Here, as an example, we'll repeat a similar
120
+ integration as above, but (1) add a positional offset of the initial conditions
121
+ from the origin, and (2) specify an external potential using the
122
+ `~gala.potential.potential.MilkyWayPotential` class as an external potential::
123
+
124
+ >>> external_pot = gp.MilkyWayPotential(version="latest")
125
+ >>> w0_1 = gd.PhaseSpacePosition(pos=[10, 0, 0] * u.kpc,
126
+ ... vel=[0, 200, 0] * u.km/u.s)
127
+ >>> w0_2 = gd.PhaseSpacePosition(pos=w0_1.xyz + [10., 0, 0] * u.pc,
128
+ ... vel=w0_1.v_xyz + [0, 5, 0] * u.km/u.s)
129
+ >>> w0 = gd.combine((w0_1, w0_2))
130
+ >>> pot1 = gp.HernquistPotential(m=1e7*u.Msun, c=0.5*u.kpc, units=galactic)
131
+ >>> particle_pot = [pot1, None]
132
+ >>> nbody = DirectNBody(w0, particle_pot, external_potential=external_pot)
133
+ >>> orbits = nbody.integrate_orbit(dt=1e-2*u.Myr, t1=0, t2=1*u.Gyr)
134
+ >>> fig, ax = plt.subplots(1, 1, figsize=(5, 5)) # doctest: +SKIP
135
+ >>> _ = orbits[:, 0].plot(['x', 'y'], axes=[ax]) # doctest: +SKIP
136
+ >>> _ = orbits[:, 1].plot(['x', 'y'], axes=[ax]) # doctest: +SKIP
137
+
138
+ .. plot::
139
+ :align: center
140
+ :context: close-figs
141
+ :width: 60%
142
+
143
+ external_pot = gp.MilkyWayPotential(version="latest")
144
+ w0_1 = gd.PhaseSpacePosition(pos=[10, 0, 0] * u.kpc,
145
+ vel=[0, 200, 0] * u.km/u.s)
146
+ w0_2 = gd.PhaseSpacePosition(pos=w0_1.xyz + [10., 0, 0] * u.pc,
147
+ vel=w0_1.v_xyz + [0, 5, 0] * u.km/u.s)
148
+ w0 = gd.combine((w0_1, w0_2))
149
+ pot1 = gp.HernquistPotential(m=1e7*u.Msun, c=0.5*u.kpc, units=galactic)
150
+ particle_pot = [pot1, None]
151
+ nbody = DirectNBody(w0, particle_pot, external_potential=external_pot)
152
+ orbits = nbody.integrate_orbit(dt=1e-2*u.Myr, t1=0, t2=1*u.Gyr)
153
+
154
+ fig, ax = plt.subplots(1, 1, figsize=(5, 5)) # doctest: +SKIP
155
+ _ = orbits[:, 0].plot(['x', 'y'], axes=[ax]) # doctest: +SKIP
156
+ _ = orbits[:, 1].plot(['x', 'y'], axes=[ax]) # doctest: +SKIP
157
+
158
+ In this case, the orbits are so similar that it is hard to tell whether the test
159
+ particle is actually bound to the secondary mass. Let's instead now plot the
160
+ position in the x-y plane of particle 2 relative to particle 1. This will look
161
+ strange because we have not transformed to the frame of particle 1, but it
162
+ should give us a sense of whether particle 2 is bound or unbound to this mass::
163
+
164
+ >>> dxyz = orbits[:, 0].xyz - orbits[:, 1].xyz
165
+ >>> fig, ax = plt.subplots(1, 1, figsize=(5, 5)) # doctest: +SKIP
166
+ >>> ax.plot(dxyz[0], dxyz[1]) # doctest: +SKIP
167
+
168
+ .. plot::
169
+ :align: center
170
+ :context: close-figs
171
+ :width: 60%
172
+
173
+ dxyz = orbits[:, 0].xyz - orbits[:, 1].xyz
174
+
175
+ fig, ax = plt.subplots(1, 1, figsize=(5, 5)) # doctest: +SKIP
176
+ ax.plot(dxyz[0], dxyz[1]) # doctest: +SKIP
177
+ ax.set_xlim(-0.1, 0.1)
178
+ ax.set_ylim(-0.1, 0.1)
179
+
180
+ From this, it looks like particle 2 is indeed still bound to particle 1 as they
181
+ both orbit within the external potential.
182
+
183
+
184
+ .. automodapi:: gala.dynamics.nbody
gala/source/docs/dynamics/nd-representations.rst ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. include:: references.txt
2
+
3
+ .. _nd-representations:
4
+
5
+ ************************************
6
+ N-dimensional representation classes
7
+ ************************************
8
+
9
+ For the examples below the following imports have already been executed::
10
+
11
+ >>> import astropy.units as u
12
+ >>> import numpy as np
13
+ >>> import gala.dynamics as gd
14
+
15
+ Introduction
16
+ ============
17
+
18
+ The Astropy |astropyrep|_ presently only support 3D positions and differential
19
+ objects. The `~gala.dynamics.representation_nd.NDCartesianRepresentation` and
20
+ `~gala.dynamics.representation_nd.NDCartesianDifferential` classes add Cartesian
21
+ representation classes that can handle arbitrary numbers of dimensions. For
22
+ example, 2D coordinates::
23
+
24
+ >>> xy = np.arange(16).reshape(2, 8) * u.kpc
25
+ >>> rep = gd.NDCartesianRepresentation(xy)
26
+ >>> rep
27
+ <NDCartesianRepresentation (x1, x2) in kpc
28
+ [(0., 8.), (1., 9.), (2., 10.), (3., 11.), (4., 12.), (5., 13.),
29
+ (6., 14.), (7., 15.)]>
30
+
31
+ 4D coordinates::
32
+
33
+ >>> x = np.arange(16).reshape(4, 4) * u.kpc
34
+ >>> rep = gd.NDCartesianRepresentation(x)
35
+ >>> rep
36
+ <NDCartesianRepresentation (x1, x2, x3, x4) in kpc
37
+ [(0., 4., 8., 12.), (1., 5., 9., 13.), (2., 6., 10., 14.),
38
+ (3., 7., 11., 15.)]>
39
+
40
+ These can be passed in to the |psp| or |orb| classes as with any of the Astropy
41
+ core representation objects::
42
+
43
+ >>> xy = np.arange(16).reshape(2, 8) * u.kpc
44
+ >>> vxy = np.arange(16).reshape(2, 8) / 10. * u.kpc / u.Myr
45
+ >>> w = gd.PhaseSpacePosition(pos=xy, vel=vxy)
46
+ >>> fig = w.plot()
47
+
48
+ .. plot::
49
+ :align: center
50
+ :width: 60%
51
+
52
+ import astropy.units as u
53
+ import numpy as np
54
+ import gala.dynamics as gd
55
+ xy = np.arange(16).reshape(2, 8) * u.kpc
56
+ vxy = np.arange(16).reshape(2, 8) / 10. * u.kpc / u.Myr
57
+ w = gd.PhaseSpacePosition(pos=xy, vel=vxy)
58
+ fig = w.plot()
59
+
60
+ However, certain functionality such as representation transformations, dynamical
61
+ quantity calculation, and coordinate frame transformations are disabled when the
62
+ number of dimensions is not 3 (i.e. when not using the Astropy core
63
+ representation classes).
64
+
65
+ N-dimensional representations API
66
+ ---------------------------------
67
+ .. automodapi:: gala.dynamics.representation_nd
68
+ :no-heading:
69
+ :headings: ^^
gala/source/docs/dynamics/nonlinear.rst ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. _gala-nonlinear-dynamics:
2
+
3
+ ******************
4
+ Nonlinear Dynamics
5
+ ******************
6
+
7
+ Introduction
8
+ ============
9
+
10
+ This module contains utilities for nonlinear dynamics. Currently, the only
11
+ implemented features enable you to compute estimates of the maximum
12
+ Lyapunov exponent for an orbit. In future releases, there will be features
13
+ for creating surface of sections and computing the full Lyapunov spectrum.
14
+
15
+ Some imports needed for the code below::
16
+
17
+ >>> import astropy.units as u
18
+ >>> import numpy as np
19
+ >>> import gala.potential as gp
20
+ >>> import gala.dynamics as gd
21
+ >>> from gala.units import galactic
22
+
23
+ Computing Lyapunov exponents
24
+ ============================
25
+
26
+ Chaotic orbit
27
+ -------------
28
+
29
+ There are two ways to compute Lyapunov exponents implemented in `gala.dynamics`.
30
+ In most cases, you'll want to use the
31
+ `~gala.dynamics.nonlinear.fast_lyapunov_max` function because the integration is
32
+ implemented in C and is quite fast. This function only works if the potential
33
+ you are working with is implemented in C (e.g., it is a
34
+ `~gala.potential.potential.CPotentialBase` subclass). With a potential object
35
+ and a set of initial conditions::
36
+
37
+ >>> pot = gp.LogarithmicPotential(v_c=150*u.km/u.s, r_h=0.1*u.kpc,
38
+ ... q1=1., q2=0.8, q3=0.6, units=galactic)
39
+ >>> w0 = gd.PhaseSpacePosition(pos=[5.5,0.,5.5]*u.kpc,
40
+ ... vel=[0.,100.,0]*u.km/u.s)
41
+ >>> lyap,orbit = gd.fast_lyapunov_max(w0, pot, dt=2., n_steps=100000) # doctest: +SKIP
42
+
43
+ This returns two objects: an `~astropy.units.Quantity` object that
44
+ contains the maximum Lyapunov exponent estimate for each offset orbit,
45
+ (we can control the number of offset orbits with the ``noffset_orbits``
46
+ argument) and an `~gala.dynamics.Orbit` object that contains
47
+ the parent orbit and each offset orbit. Let's plot the parent orbit::
48
+
49
+ >>> fig = orbit[:,0].plot(marker=',', alpha=0.25, linestyle='none') # doctest: +SKIP
50
+
51
+ .. plot::
52
+ :align: center
53
+
54
+ import astropy.units as u
55
+ import matplotlib.pyplot as plt
56
+ import gala.potential as gp
57
+ import gala.dynamics as gd
58
+ from gala.units import galactic
59
+
60
+ pot = gp.LogarithmicPotential(v_c=150*u.km/u.s, r_h=0.1*u.kpc,
61
+ q1=1., q2=0.8, q3=0.6, units=galactic)
62
+ w0 = gd.PhaseSpacePosition(pos=[5.5,0.,5.5]*u.kpc,
63
+ vel=[0.,100.,0]*u.km/u.s)
64
+ lyap,orbit = gd.fast_lyapunov_max(w0, pot, dt=2., n_steps=100000)
65
+ fig = orbit[:,0].plot(marker=',', linestyle='none', alpha=0.25)
66
+
67
+ Visually, this looks like a chaotic orbit. This means the Lyapunov exponent
68
+ should saturate to some value. We'll now plot the estimate of the Lyapunov
69
+ exponent as a function of time -- because the algorithm re-normalizes every
70
+ several time-steps (controllable with the ``n_steps_per_pullback`` argument),
71
+ we have to down-sample the time array to align it with the Lyapunov exponent
72
+ array. This plots one line per offset orbit::
73
+
74
+ >>> plt.figure() # doctest: +SKIP
75
+ >>> plt.loglog(orbit.t[11::10], lyap, marker='') # doctest: +SKIP
76
+ >>> plt.xlabel("Time [{}]".format(orbit.t.unit)) # doctest: +SKIP
77
+ >>> plt.ylabel(r"$\lambda_{{\rm max}}$ [{}]".format(lyap.unit)) # doctest: +SKIP
78
+ >>> plt.tight_layout() # doctest: +SKIP
79
+
80
+ .. plot::
81
+ :align: center
82
+ :width: 60%
83
+
84
+ import astropy.units as u
85
+ import matplotlib.pyplot as plt
86
+ import gala.potential as gp
87
+ import gala.dynamics as gd
88
+ from gala.units import galactic
89
+
90
+ pot = gp.LogarithmicPotential(v_c=150*u.km/u.s, r_h=0.1*u.kpc,
91
+ q1=1., q2=0.8, q3=0.6, units=galactic)
92
+ w0 = gd.PhaseSpacePosition(pos=[5.5,0.,5.5]*u.kpc,
93
+ vel=[0.,100.,0]*u.km/u.s)
94
+ lyap,orbit = gd.fast_lyapunov_max(w0, pot, dt=2., n_steps=100000)
95
+
96
+ plt.figure()
97
+ plt.loglog(orbit.t[11::10], lyap, marker='')
98
+ plt.xlabel("Time [{}]".format(orbit.t.unit))
99
+ plt.ylabel(r"$\lambda_{{\rm max}}$ [{}]".format(lyap.unit))
100
+ plt.tight_layout()
101
+
102
+ The estimate is clearly starting to diverge from a simple power law decay.
103
+
104
+ Regular orbit
105
+ -------------
106
+
107
+ To compare, we will compute the estimate for a regular orbit as well::
108
+
109
+ >>> w0 = gd.PhaseSpacePosition(pos=[5.5,0.,0.]*u.kpc,
110
+ ... vel=[0.,140.,25]*u.km/u.s)
111
+ >>> lyap,orbit = gd.fast_lyapunov_max(w0, pot, dt=2., n_steps=100000) # doctest: +SKIP
112
+ >>> fig = orbit[:,0].plot(marker=',', alpha=0.1, linestyle='none') # doctest: +SKIP
113
+
114
+ .. plot::
115
+ :align: center
116
+
117
+ import astropy.units as u
118
+ import gala.potential as gp
119
+ import gala.dynamics as gd
120
+ from gala.units import galactic
121
+
122
+ pot = gp.LogarithmicPotential(v_c=150*u.km/u.s, r_h=0.1*u.kpc,
123
+ q1=1., q2=0.8, q3=0.6, units=galactic)
124
+ w0 = gd.PhaseSpacePosition(pos=[5.5,0.,0.]*u.kpc,
125
+ vel=[0.,140.,25]*u.km/u.s)
126
+ lyap,orbit = gd.fast_lyapunov_max(w0, pot, dt=1., n_steps=200000)
127
+ fig = orbit[:,0].plot(marker=',', linestyle='none', alpha=0.1)
128
+
129
+ Because this is a regular orbit, the estimate continues decreasing,
130
+ following a characteristic power-law (a straight line in a log-log plot)::
131
+
132
+ >>> pl.figure() # doctest: +SKIP
133
+ >>> pl.loglog(orbit.t[11::10], lyap, marker='') # doctest: +SKIP
134
+ >>> pl.xlabel("Time [{}]".format(orbit.t.unit)) # doctest: +SKIP
135
+ >>> pl.ylabel(r"$\lambda_{{\rm max}}$ [{}]".format(lyap.unit)) # doctest: +SKIP
136
+ >>> pl.tight_layout() # doctest: +SKIP
137
+
138
+ .. plot::
139
+ :align: center
140
+ :width: 60%
141
+
142
+ import astropy.units as u
143
+ import matplotlib.pyplot as pl
144
+ import gala.potential as gp
145
+ import gala.dynamics as gd
146
+ from gala.units import galactic
147
+
148
+ pot = gp.LogarithmicPotential(v_c=150*u.km/u.s, r_h=0.1*u.kpc,
149
+ q1=1., q2=0.8, q3=0.6, units=galactic)
150
+ w0 = gd.PhaseSpacePosition(pos=[5.5,0.,0.]*u.kpc,
151
+ vel=[0.,140.,25]*u.km/u.s)
152
+ lyap,orbit = gd.fast_lyapunov_max(w0, pot, dt=1., n_steps=200000)
153
+
154
+ pl.figure()
155
+ pl.loglog(orbit.t[11::10], lyap, marker='')
156
+ pl.xlabel("Time [{}]".format(orbit.t.unit))
157
+ pl.ylabel(r"$\lambda_{{\rm max}}$ [{}]".format(lyap.unit))
158
+ pl.tight_layout()
159
+
160
+ API
161
+ ---
162
+ .. automodapi:: gala.dynamics.nonlinear
163
+ :no-heading:
164
+ :headings: ^^
gala/source/docs/dynamics/orbits-in-detail.rst ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. include:: references.txt
2
+
3
+ .. _orbits-in-detail:
4
+
5
+ *****************************************************
6
+ Orbit and phase-space position objects in more detail
7
+ *****************************************************
8
+
9
+ For the examples below the following imports have already been executed::
10
+
11
+ >>> import astropy.units as u
12
+ >>> import numpy as np
13
+ >>> import gala.potential as gp
14
+ >>> import gala.dynamics as gd
15
+ >>> from astropy.coordinates import (CylindricalRepresentation,
16
+ ... CylindricalDifferential)
17
+ >>> from gala.units import galactic
18
+ >>> np.random.seed(42)
19
+
20
+ We will also set the default Astropy Galactocentric frame parameters to the
21
+ values adopted in Astropy v4.0:
22
+
23
+ >>> import astropy.coordinates as coord
24
+ >>> _ = coord.galactocentric_frame_defaults.set('v4.0')
25
+
26
+ Introduction
27
+ ============
28
+
29
+ The `astropy.units` subpackage is excellent for working with numbers and
30
+ associated units, but dynamical quantities often contain many quantities with
31
+ mixed units. An example is a position in phase-space, which may contain some
32
+ quantities with length units and some quantities with velocity or momentum
33
+ units. The |psp| and |orb| classes are designed to work with these data
34
+ structures and provide a consistent API for visualizing and computing further
35
+ dynamical quantities. Click these shortcuts to jump to a section below, or start
36
+ reading below:
37
+
38
+ * :ref:`phase-space-position`
39
+ * :ref:`orbit`
40
+
41
+ .. _phase-space-position:
42
+
43
+ Phase-space Positions
44
+ =====================
45
+
46
+ The |psp| class provides an interface for representing full phase-space
47
+ positions--coordinate positions and momenta (velocities). This class is useful
48
+ as a container for initial conditions and for transforming phase-space positions
49
+ to new coordinate representations or reference frames.
50
+
51
+ The easiest way to create a |psp| object is to pass in a pair of
52
+ `~astropy.units.Quantity` objects that represent the Cartesian position and
53
+ velocity vectors::
54
+
55
+ >>> gd.PhaseSpacePosition(pos=[4., 8., 15.] * u.kpc,
56
+ ... vel=[-150., 50., 15.] * u.km/u.s)
57
+ <PhaseSpacePosition cartesian, dim=3, shape=()>
58
+
59
+ By default, passing in `~astropy.units.Quantity`'s are interpreted as Cartesian
60
+ coordinates and velocities. This works with arrays of positions and velocities
61
+ as well::
62
+
63
+ >>> x = np.arange(24).reshape(3, 8)
64
+ >>> v = np.arange(24).reshape(3, 8)
65
+ >>> w = gd.PhaseSpacePosition(pos=x * u.kpc,
66
+ ... vel=v * u.km/u.s)
67
+ >>> w
68
+ <PhaseSpacePosition cartesian, dim=3, shape=(8,)>
69
+
70
+ This is interpreted as 8, 6-dimensional phase-space positions.
71
+
72
+ The class internally stores the positions and velocities as
73
+ `~astropy.coordinates.BaseRepresentation` and
74
+ `~astropy.coordinates.BaseDifferential` subclasses; in this case,
75
+ `~astropy.coordinates.CartesianRepresentation` and
76
+ `~astropy.coordinates.CartesianDifferential`::
77
+
78
+ >>> w.pos
79
+ <CartesianRepresentation (x, y, z) in kpc
80
+ [(0., 8., 16.), (1., 9., 17.), (2., 10., 18.), (3., 11., 19.),
81
+ (4., 12., 20.), (5., 13., 21.), (6., 14., 22.), (7., 15., 23.)]>
82
+ >>> w.vel
83
+ <CartesianDifferential (d_x, d_y, d_z) in km / s
84
+ [(0., 8., 16.), (1., 9., 17.), (2., 10., 18.), (3., 11., 19.),
85
+ (4., 12., 20.), (5., 13., 21.), (6., 14., 22.), (7., 15., 23.)]>
86
+
87
+ All of the components of these classes are mapped to attributes of the
88
+ phase-space position class for convenience, but with more user-friendly names.
89
+ These mappings are defined in the class definition of
90
+ `~gala.dynamics.PhaseSpacePosition`. For example, to access the ``x`` component
91
+ of the position and the ``v_x`` component of the velocity::
92
+
93
+ >>> w.x # doctest: +FLOAT_CMP
94
+ <Quantity [0.,1.,2.,3.,4.,5.,6.,7.] kpc>
95
+ >>> w.v_x # doctest: +FLOAT_CMP
96
+ <Quantity [0.,1.,2.,3.,4.,5.,6.,7.] km / s>
97
+
98
+ The default representation is Cartesian, but the class can also be instantiated
99
+ with representation objects instead of `~astropy.units.Quantity`'s -- this is
100
+ useful for creating |psp| or |orb| instances from non-Cartesian
101
+ representations of the position and velocity::
102
+
103
+ >>> pos = CylindricalRepresentation(rho=np.linspace(1., 4, 4) * u.kpc,
104
+ ... phi=np.linspace(0, np.pi, 4) * u.rad,
105
+ ... z=np.linspace(-1, 1., 4) * u.kpc)
106
+ >>> vel = CylindricalDifferential(d_rho=np.linspace(100, 150, 4) * u.km/u.s,
107
+ ... d_phi=np.linspace(-1, 1, 4) * u.rad/u.Myr,
108
+ ... d_z=np.linspace(-15, 15., 4) * u.km/u.s)
109
+ >>> w = gd.PhaseSpacePosition(pos=pos, vel=vel)
110
+ >>> w
111
+ <PhaseSpacePosition cylindrical, dim=3, shape=(4,)>
112
+ >>> w.rho
113
+ <Quantity [1., 2., 3., 4.] kpc>
114
+
115
+ We can easily transform the full phase-space vector to new representations or
116
+ coordinate frames. These transformations use the :mod:`astropy.coordinates`
117
+ |astropyrep|_::
118
+
119
+ >>> cart = w.represent_as('cartesian')
120
+ >>> cart.x
121
+ <Quantity [ 1. , 1. , -1.5, -4. ] kpc>
122
+ >>> sph = w.represent_as('spherical')
123
+ >>> sph.distance
124
+ <Distance [1.41421356, 2.02758751, 3.01846171, 4.12310563] kpc>
125
+
126
+ There is also support for transforming the positions and velocities (assumed to
127
+ be in a `~astropy.coordinates.Galactocentric` frame) to any of the other
128
+ coordinate frames. For example, to transform to
129
+ :class:`~astropy.coordinates.Galactic` coordinates::
130
+
131
+ >>> from astropy.coordinates import Galactic
132
+ >>> gal_c = w.to_coord_frame(Galactic())
133
+ >>> gal_c # doctest: +FLOAT_CMP
134
+ <Galactic Coordinate: (l, b, distance) in (deg, deg, kpc)
135
+ [(4.40971301e-05, -6.23850462, 9.17891228),
136
+ (1.07501936e+01, -2.04017409, 9.29170644),
137
+ (2.14246214e+01, 2.65220588, 7.12026744),
138
+ (7.35169893e-05, 13.50991169, 4.23668468)]
139
+ (pm_l_cosb, pm_b, radial_velocity) in (mas / yr, mas / yr, km / s)
140
+ [( -28.11596908, -0.297625 , 89.093095 ),
141
+ ( -13.077309 , 0.15891073, 511.60269726),
142
+ ( -7.04751509, 1.33976418, -1087.52574084),
143
+ (-206.97042166, 2.22471526, -156.82064814)]>
144
+
145
+ We can easily plot projections of the phase-space positions using the
146
+ `~gala.dynamics.PhaseSpacePosition.plot` method::
147
+
148
+ >>> np.random.seed(42)
149
+ >>> x = np.random.uniform(-10, 10, size=(3,128))
150
+ >>> v = np.random.uniform(-200, 200, size=(3,128))
151
+ >>> w = gd.PhaseSpacePosition(pos=x * u.kpc,
152
+ ... vel=v * u.km/u.s)
153
+ >>> fig = w.plot() # doctest: +SKIP
154
+
155
+ .. plot::
156
+ :align: center
157
+ :context: close-figs
158
+
159
+ import astropy.units as u
160
+ import numpy as np
161
+ import gala.dynamics as gd
162
+
163
+ np.random.seed(42)
164
+ x = np.random.uniform(-10,10,size=(3,128))
165
+ v = np.random.uniform(-200,200,size=(3,128))
166
+ w = gd.PhaseSpacePosition(pos=x*u.kpc,
167
+ vel=v*u.km/u.s)
168
+ fig = w.plot()
169
+
170
+ This is a thin wrapper around the `~gala.dynamics.plot_projections`
171
+ function and any keyword arguments are passed through to that function::
172
+
173
+ >>> fig = w.plot(components=['x', 'v_z'], color='r',
174
+ ... facecolor='none', marker='o', s=20, alpha=0.5) # doctest: +SKIP
175
+
176
+ .. plot::
177
+ :align: center
178
+ :context: close-figs
179
+ :width: 60%
180
+
181
+ fig = w.plot(components=['x', 'v_z'], color='r',
182
+ facecolor='none', marker='o', s=20, alpha=0.5)
183
+
184
+
185
+ .. _orbit:
186
+
187
+ Orbits
188
+ ======
189
+
190
+ The |orb| class inherits much of the functionality from |psp| (described above)
191
+ and adds some additional features that are useful for time-series orbits.
192
+
193
+ An |orb| instance is initialized like the |psp|--with arrays of positions and
194
+ velocities-- but usually also requires specifying a time array as well. Also,
195
+ the extra axes in these arrays hold special meaning for the |orb| class. The
196
+ position and velocity arrays passed to |psp| can have arbitrary numbers of
197
+ dimensions as long as the 0th axis specifies the dimensionality. For the |orb|
198
+ class, the 0th axis remains the axis of dimensionality, but the 1st axis now is
199
+ always assumed to be the time axis. For example, an input position with shape
200
+ ``(2,128)`` to a |psp| represents 128 independent 2D positions, but to a |orb|
201
+ it represents a single orbit's positions at 128 times::
202
+
203
+ >>> t = np.linspace(0, 100, 128) * u.Myr
204
+ >>> Om = 1E-1 * u.rad / u.Myr
205
+ >>> pos = np.vstack((5*np.cos(Om*t), np.sin(Om*t))).value * u.kpc
206
+ >>> vel = np.vstack((-5*np.sin(Om*t), np.cos(Om*t))).value * u.kpc/u.Myr
207
+ >>> orbit = gd.Orbit(pos=pos, vel=vel)
208
+ >>> orbit
209
+ <Orbit ndcartesian, dim=2, shape=(128,)>
210
+
211
+ To create a single object that contains multiple orbits, the input position
212
+ object should have 3 axes. The last axis (``axis=2``) specifies the number of
213
+ orbits. So, an input position with shape ``(2,128,16)`` would represent 16, 2D
214
+ orbits, each with the same 128 times::
215
+
216
+ >>> t = np.linspace(0, 100, 128) * u.Myr
217
+ >>> Om = np.random.uniform(size=16) * u.rad / u.Myr
218
+ >>> angle = Om[None] * t[:, None]
219
+ >>> pos = np.stack((5*np.cos(angle), np.sin(angle))).value * u.kpc
220
+ >>> vel = np.stack((-5*np.sin(angle), np.cos(angle))).value * u.kpc/u.Myr
221
+ >>> orbit = gd.Orbit(pos=pos, vel=vel)
222
+ >>> orbit
223
+ <Orbit ndcartesian, dim=2, shape=(128, 16)>
224
+
225
+ To make full use of the orbit functionality, you must also pass in an array with
226
+ the time values and an instance of a `~gala.potential.potential.PotentialBase`
227
+ subclass that represents the potential that the orbit was integrated in::
228
+
229
+ >>> pot = gp.PlummerPotential(m=1E10, b=1., units=galactic)
230
+ >>> orbit = gd.Orbit(pos=pos*u.kpc, vel=vel*u.km/u.s,
231
+ ... t=t*u.Myr, potential=pot)
232
+
233
+ (note, in this case ``pos`` and ``vel`` were not generated from integrating
234
+ an orbit in the potential ``pot``!). However, most of the time you won't need to
235
+ create |orb| objects from scratch! They are returned from any of the numerical
236
+ integration routines provided in `gala`. For example, they are returned by the
237
+ `~gala.potential.potential.PotentialBase.integrate_orbit` method of potential
238
+ objects and will automatically contain the ``time`` array and ``potential``
239
+ object. For example::
240
+
241
+ >>> pot = gp.PlummerPotential(m=1E10 * u.Msun, b=1. * u.kpc, units=galactic)
242
+ >>> w0 = gd.PhaseSpacePosition(pos=[10.,0,0] * u.kpc,
243
+ ... vel=[0.,75,0] * u.km/u.s)
244
+ >>> orbit = gp.Hamiltonian(pot).integrate_orbit(w0, dt=1., n_steps=5000)
245
+ >>> orbit
246
+ <Orbit cartesian, dim=3, shape=(5001,)>
247
+ >>> orbit.t
248
+ <Quantity [0.000e+00, 1.000e+00, 2.000e+00, ..., 4.998e+03, 4.999e+03,
249
+ 5.000e+03] Myr>
250
+ >>> orbit.potential
251
+ <PlummerPotential: m=1.00e+10, b=1.00 (kpc,Myr,solMass,rad)>
252
+
253
+ Just like for |psp|, we can quickly visualize an orbit using the
254
+ `~gala.dynamics.Orbit.plot` method::
255
+
256
+ >>> fig = orbit.plot() # doctest: +SKIP
257
+
258
+ .. plot::
259
+ :align: center
260
+ :context: close-figs
261
+
262
+ import astropy.units as u
263
+ import gala.dynamics as gd
264
+ import gala.potential as gp
265
+ from gala.units import galactic
266
+
267
+ pot = gp.PlummerPotential(m=1E10 * u.Msun, b=1. * u.kpc, units=galactic)
268
+ w0 = gd.PhaseSpacePosition(pos=[2.,0,0] * u.kpc,
269
+ vel=[0.,75,15] * u.km/u.s)
270
+ orbit = gp.Hamiltonian(pot).integrate_orbit(w0, dt=1., n_steps=5000)
271
+ fig = orbit.plot()
272
+
273
+ Again, this is a thin wrapper around the `~gala.dynamics.plot_projections`
274
+ function and any keyword arguments are passed through to that function::
275
+
276
+ >>> fig = orbit.plot(linewidth=4., alpha=0.5, color='r') # doctest: +SKIP
277
+
278
+ .. plot::
279
+ :align: center
280
+ :context: close-figs
281
+
282
+ fig = orbit.plot(linewidth=4., alpha=0.5, color='r')
283
+
284
+ Alternatively, for three-dimensional orbits, we can visualize the orbit using
285
+ the 3D projection capabilities in `matplotlib`::
286
+
287
+ >>> fig = orbit.plot_3d(alpha=0.5, color='k') # doctest: +SKIP
288
+
289
+ .. plot::
290
+ :align: center
291
+ :context: close-figs
292
+ :width: 60%
293
+
294
+ fig = orbit.plot_3d(alpha=0.5, color='k')
295
+
296
+ We can also quickly create an animation of the progression of an orbit using the
297
+ `~gala.dynamics.Orbit.animate` method, which animated projections of the orbit::
298
+
299
+ >>> fig, anim = orbit[:1000].animate(stride=10) # doctest: +SKIP
300
+
301
+ .. raw:: html
302
+
303
+ <video controls src="../_static/orbit-anim1.mp4" width=650 autoplay loop></video>
304
+
305
+ The animate method acts like `~gala.dynamics.Orbit.plot`, in that it works for
306
+ any coordinate representation (Cartesian, cylindrical, etc.) and supports only
307
+ animating subsets of the phase-space components. For example, to make an
308
+ animation of an orbit in cylindrical coordinates, showing the orbit proress in
309
+ the R,z meridional plane::
310
+
311
+ >>> fig, anim = orbit[:1000].cylindrical.animate(components=['rho', 'z'], # doctest: +SKIP
312
+ ... stride=10)
313
+
314
+ .. raw:: html
315
+
316
+ <video controls src="../_static/orbit-anim2.mp4" width=450 autoplay loop></video>
317
+
318
+ We can also quickly compute quantities like the angular momentum, and estimates
319
+ for the pericenter, apocenter, eccentricity of the orbit. Estimates for the
320
+ latter few get better with smaller timesteps::
321
+
322
+ >>> orbit = gp.Hamiltonian(pot).integrate_orbit(w0, dt=0.1, n_steps=100000)
323
+ >>> np.mean(orbit.angular_momentum(), axis=1) # doctest: +FLOAT_CMP
324
+ <Quantity [0. ,0. ,0.76703412] kpc2 / Myr>
325
+ >>> orbit.eccentricity() # doctest: +FLOAT_CMP
326
+ <Quantity 0.31951765618193967>
327
+ >>> orbit.pericenter() # doctest: +FLOAT_CMP
328
+ <Quantity 10.00000005952518 kpc>
329
+ >>> orbit.apocenter() # doctest: +FLOAT_CMP
330
+ <Quantity 19.390916871970223 kpc>
331
+
332
+
333
+ More information
334
+ ================
335
+
336
+ Internally, both of the above classes rely on the Astropy representation
337
+ transformation framework (i.e. the subclasses of
338
+ `~astropy.coordinates.BaseRepresentation` and
339
+ `~astropy.coordinates.BaseDifferential`). However, at present these classes only
340
+ support 3D positions and differentials (velocities). The |psp| and |orb| classes
341
+ both support arbitrary numbers of dimensions and, when relevant, rely on custom
342
+ subclasses of the representation classes to handle such cases. See the
343
+ :ref:`nd-representations` page for more information about these classes.