unisky 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. unisky-0.2.0/.gitignore +21 -0
  2. unisky-0.2.0/CHANGELOG.md +386 -0
  3. unisky-0.2.0/LICENSE +29 -0
  4. unisky-0.2.0/PKG-INFO +653 -0
  5. unisky-0.2.0/README.md +616 -0
  6. unisky-0.2.0/docs/alignment.md +347 -0
  7. unisky-0.2.0/docs/composites.md +289 -0
  8. unisky-0.2.0/docs/deploy.md +1047 -0
  9. unisky-0.2.0/docs/frontend-bundle.md +126 -0
  10. unisky-0.2.0/docs/incremental.md +696 -0
  11. unisky-0.2.0/docs/lensing-normalization.md +79 -0
  12. unisky-0.2.0/docs/prerendered.md +326 -0
  13. unisky-0.2.0/docs/remote-data.md +1355 -0
  14. unisky-0.2.0/docs/superpowers/plans/2026-08-20-anymap-api.md +1953 -0
  15. unisky-0.2.0/docs/superpowers/plans/2026-08-20-anymap-frontend.md +4625 -0
  16. unisky-0.2.0/docs/superpowers/plans/2026-08-20-anymap-integration.md +1891 -0
  17. unisky-0.2.0/docs/superpowers/plans/2026-08-20-anymap-python-core.md +3321 -0
  18. unisky-0.2.0/docs/superpowers/plans/2026-08-20-unisky-interfaces.md +1152 -0
  19. unisky-0.2.0/docs/superpowers/specs/2026-08-20-anymap-design.md +327 -0
  20. unisky-0.2.0/docs/uploads.md +304 -0
  21. unisky-0.2.0/examples/rxcj2211.yaml +677 -0
  22. unisky-0.2.0/examples/rxcj2211_core.yaml +212 -0
  23. unisky-0.2.0/frontend/e2e/viewer.spec.ts +739 -0
  24. unisky-0.2.0/frontend/index.html +66 -0
  25. unisky-0.2.0/frontend/package-lock.json +2529 -0
  26. unisky-0.2.0/frontend/package.json +25 -0
  27. unisky-0.2.0/frontend/playwright.config.ts +66 -0
  28. unisky-0.2.0/frontend/public/apple-touch-icon.png +0 -0
  29. unisky-0.2.0/frontend/public/favicon-16.png +0 -0
  30. unisky-0.2.0/frontend/public/favicon-32.png +0 -0
  31. unisky-0.2.0/frontend/public/favicon-48.png +0 -0
  32. unisky-0.2.0/frontend/public/favicon.ico +0 -0
  33. unisky-0.2.0/frontend/public/favicon.svg +15 -0
  34. unisky-0.2.0/frontend/src/api.ts +170 -0
  35. unisky-0.2.0/frontend/src/capabilities.ts +123 -0
  36. unisky-0.2.0/frontend/src/catalog.ts +416 -0
  37. unisky-0.2.0/frontend/src/colormaps.json +1 -0
  38. unisky-0.2.0/frontend/src/colormaps.ts +36 -0
  39. unisky-0.2.0/frontend/src/crs.ts +105 -0
  40. unisky-0.2.0/frontend/src/datatiles.ts +902 -0
  41. unisky-0.2.0/frontend/src/main.ts +862 -0
  42. unisky-0.2.0/frontend/src/manifest.ts +51 -0
  43. unisky-0.2.0/frontend/src/permalink.ts +390 -0
  44. unisky-0.2.0/frontend/src/rgb.ts +760 -0
  45. unisky-0.2.0/frontend/src/state.ts +1456 -0
  46. unisky-0.2.0/frontend/src/stretch.ts +187 -0
  47. unisky-0.2.0/frontend/src/styles.css +1364 -0
  48. unisky-0.2.0/frontend/src/types.ts +463 -0
  49. unisky-0.2.0/frontend/src/ui/about.ts +0 -0
  50. unisky-0.2.0/frontend/src/ui/contrast-drag.css +64 -0
  51. unisky-0.2.0/frontend/src/ui/contrast-drag.ts +736 -0
  52. unisky-0.2.0/frontend/src/ui/coords.ts +55 -0
  53. unisky-0.2.0/frontend/src/ui/cutout-tool.ts +536 -0
  54. unisky-0.2.0/frontend/src/ui/dock.ts +313 -0
  55. unisky-0.2.0/frontend/src/ui/goto.ts +79 -0
  56. unisky-0.2.0/frontend/src/ui/layer-tree.ts +1287 -0
  57. unisky-0.2.0/frontend/src/ui/layers-panel.ts +207 -0
  58. unisky-0.2.0/frontend/src/ui/lens-panel.ts +654 -0
  59. unisky-0.2.0/frontend/src/ui/menubar.ts +578 -0
  60. unisky-0.2.0/frontend/src/ui/mode-banner.ts +188 -0
  61. unisky-0.2.0/frontend/src/ui/popup.ts +291 -0
  62. unisky-0.2.0/frontend/src/ui/rgb-panel.ts +738 -0
  63. unisky-0.2.0/frontend/src/ui/scale-bar.ts +233 -0
  64. unisky-0.2.0/frontend/src/ui/stretch-panel.ts +205 -0
  65. unisky-0.2.0/frontend/src/ui/toast.ts +51 -0
  66. unisky-0.2.0/frontend/src/ui/upload-panel.ts +679 -0
  67. unisky-0.2.0/frontend/src/ui/window.ts +870 -0
  68. unisky-0.2.0/frontend/src/ui/zoom-slider.ts +211 -0
  69. unisky-0.2.0/frontend/src/upload.ts +1561 -0
  70. unisky-0.2.0/frontend/src/vector.ts +350 -0
  71. unisky-0.2.0/frontend/src/wcs.ts +240 -0
  72. unisky-0.2.0/frontend/src/zoom.ts +209 -0
  73. unisky-0.2.0/frontend/tests/about.test.ts +260 -0
  74. unisky-0.2.0/frontend/tests/busy.test.ts +59 -0
  75. unisky-0.2.0/frontend/tests/capabilities.test.ts +159 -0
  76. unisky-0.2.0/frontend/tests/catalog.test.ts +820 -0
  77. unisky-0.2.0/frontend/tests/contrast-drag.test.ts +860 -0
  78. unisky-0.2.0/frontend/tests/coords.test.ts +110 -0
  79. unisky-0.2.0/frontend/tests/crs.test.ts +90 -0
  80. unisky-0.2.0/frontend/tests/cutout-tool.test.ts +410 -0
  81. unisky-0.2.0/frontend/tests/datatiles.test.ts +550 -0
  82. unisky-0.2.0/frontend/tests/deployment.test.ts +673 -0
  83. unisky-0.2.0/frontend/tests/dock.test.ts +808 -0
  84. unisky-0.2.0/frontend/tests/fixtures/wcs_vectors.json +2235 -0
  85. unisky-0.2.0/frontend/tests/goto.test.ts +113 -0
  86. unisky-0.2.0/frontend/tests/layer-tree.test.ts +905 -0
  87. unisky-0.2.0/frontend/tests/layers-panel.test.ts +182 -0
  88. unisky-0.2.0/frontend/tests/lens-panel.test.ts +661 -0
  89. unisky-0.2.0/frontend/tests/main-wiring.test.ts +835 -0
  90. unisky-0.2.0/frontend/tests/manifest.test.ts +66 -0
  91. unisky-0.2.0/frontend/tests/menubar.test.ts +451 -0
  92. unisky-0.2.0/frontend/tests/mode-banner.test.ts +208 -0
  93. unisky-0.2.0/frontend/tests/node-fs.d.ts +36 -0
  94. unisky-0.2.0/frontend/tests/permalink.test.ts +433 -0
  95. unisky-0.2.0/frontend/tests/popup.test.ts +532 -0
  96. unisky-0.2.0/frontend/tests/rgb-panel.test.ts +725 -0
  97. unisky-0.2.0/frontend/tests/rgb.test.ts +677 -0
  98. unisky-0.2.0/frontend/tests/scale-bar.test.ts +253 -0
  99. unisky-0.2.0/frontend/tests/shell.test.ts +31 -0
  100. unisky-0.2.0/frontend/tests/state.test.ts +1385 -0
  101. unisky-0.2.0/frontend/tests/stretch-panel.test.ts +171 -0
  102. unisky-0.2.0/frontend/tests/stretch.test.ts +222 -0
  103. unisky-0.2.0/frontend/tests/theme.test.ts +365 -0
  104. unisky-0.2.0/frontend/tests/toast.test.ts +85 -0
  105. unisky-0.2.0/frontend/tests/types.test.ts +148 -0
  106. unisky-0.2.0/frontend/tests/upload.test.ts +1299 -0
  107. unisky-0.2.0/frontend/tests/vector.test.ts +443 -0
  108. unisky-0.2.0/frontend/tests/wcs.test.ts +207 -0
  109. unisky-0.2.0/frontend/tests/window.test.ts +564 -0
  110. unisky-0.2.0/frontend/tests/zoom-slider.test.ts +324 -0
  111. unisky-0.2.0/frontend/tests/zoom.test.ts +287 -0
  112. unisky-0.2.0/frontend/tsconfig.json +16 -0
  113. unisky-0.2.0/frontend/vite.config.ts +29 -0
  114. unisky-0.2.0/justfile +78 -0
  115. unisky-0.2.0/pyproject.toml +101 -0
  116. unisky-0.2.0/scripts/dev_build_synthetic.py +195 -0
  117. unisky-0.2.0/scripts/gen_colormaps.py +44 -0
  118. unisky-0.2.0/scripts/gen_wcs_vectors.py +94 -0
  119. unisky-0.2.0/scripts/make_favicon.py +68 -0
  120. unisky-0.2.0/scripts/make_test_field.py +336 -0
  121. unisky-0.2.0/src/unisky/__init__.py +6 -0
  122. unisky-0.2.0/src/unisky/align.py +737 -0
  123. unisky-0.2.0/src/unisky/build.py +3147 -0
  124. unisky-0.2.0/src/unisky/cli.py +553 -0
  125. unisky-0.2.0/src/unisky/config.py +2089 -0
  126. unisky-0.2.0/src/unisky/encoding.py +102 -0
  127. unisky-0.2.0/src/unisky/grid.py +388 -0
  128. unisky-0.2.0/src/unisky/layers/__init__.py +0 -0
  129. unisky-0.2.0/src/unisky/layers/catalog.py +411 -0
  130. unisky-0.2.0/src/unisky/layers/composite.py +417 -0
  131. unisky-0.2.0/src/unisky/layers/contour.py +61 -0
  132. unisky-0.2.0/src/unisky/layers/footprint.py +66 -0
  133. unisky-0.2.0/src/unisky/layers/image.py +121 -0
  134. unisky-0.2.0/src/unisky/layers/lensmodel.py +203 -0
  135. unisky-0.2.0/src/unisky/layers/prerendered.py +491 -0
  136. unisky-0.2.0/src/unisky/layers/region.py +138 -0
  137. unisky-0.2.0/src/unisky/lensing.py +464 -0
  138. unisky-0.2.0/src/unisky/manifest.py +461 -0
  139. unisky-0.2.0/src/unisky/progress.py +1066 -0
  140. unisky-0.2.0/src/unisky/remote.py +954 -0
  141. unisky-0.2.0/src/unisky/server/__init__.py +1 -0
  142. unisky-0.2.0/src/unisky/server/app.py +290 -0
  143. unisky-0.2.0/src/unisky/server/cutout.py +361 -0
  144. unisky-0.2.0/src/unisky/server/lens_api.py +107 -0
  145. unisky-0.2.0/src/unisky/sources/__init__.py +116 -0
  146. unisky-0.2.0/src/unisky/sources/footprints.py +1245 -0
  147. unisky-0.2.0/src/unisky/sources/hips.py +1036 -0
  148. unisky-0.2.0/src/unisky/sources/legacysurvey.py +803 -0
  149. unisky-0.2.0/src/unisky/static/apple-touch-icon.png +0 -0
  150. unisky-0.2.0/src/unisky/static/assets/index-BpPCJ8ql.js +344 -0
  151. unisky-0.2.0/src/unisky/static/assets/index-CAJd-K9y.css +1 -0
  152. unisky-0.2.0/src/unisky/static/bundle-source.json +70 -0
  153. unisky-0.2.0/src/unisky/static/favicon-16.png +0 -0
  154. unisky-0.2.0/src/unisky/static/favicon-32.png +0 -0
  155. unisky-0.2.0/src/unisky/static/favicon-48.png +0 -0
  156. unisky-0.2.0/src/unisky/static/favicon.ico +0 -0
  157. unisky-0.2.0/src/unisky/static/favicon.svg +15 -0
  158. unisky-0.2.0/src/unisky/static/index.html +67 -0
  159. unisky-0.2.0/src/unisky/tiler.py +678 -0
  160. unisky-0.2.0/tests/__init__.py +0 -0
  161. unisky-0.2.0/tests/conftest.py +332 -0
  162. unisky-0.2.0/tests/fixtures/all_layer_types/config.yaml +244 -0
  163. unisky-0.2.0/tests/fixtures/all_layer_types/rgb.png +0 -0
  164. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/alma_clean.fits +0 -0
  165. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/alma_pbcor.fits +0 -0
  166. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/config.yaml +83 -0
  167. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/jwst_f090w.fits +0 -0
  168. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/jwst_f200w.fits +0 -0
  169. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/jwst_f444w.fits +0 -0
  170. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/lens_alpha_x.fits +0 -0
  171. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/lens_alpha_y.fits +0 -0
  172. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/lens_gamma1.fits +0 -0
  173. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/lens_gamma2.fits +0 -0
  174. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/lens_kappa.fits +0 -0
  175. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/lens_psi.fits +0 -0
  176. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/mulimage.reg +149 -0
  177. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/mulimage.txt +147 -0
  178. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/phot_cut.fits +7 -0
  179. unisky-0.2.0/tests/fixtures/rxcj2211_tiny/photz_cut.ecsv +70 -0
  180. unisky-0.2.0/tests/fixtures/wcs_vectors.json +2235 -0
  181. unisky-0.2.0/tests/test_align.py +1169 -0
  182. unisky-0.2.0/tests/test_build.py +1658 -0
  183. unisky-0.2.0/tests/test_bundle_freshness.py +293 -0
  184. unisky-0.2.0/tests/test_catalog.py +683 -0
  185. unisky-0.2.0/tests/test_cli.py +1234 -0
  186. unisky-0.2.0/tests/test_composite.py +1321 -0
  187. unisky-0.2.0/tests/test_config.py +1132 -0
  188. unisky-0.2.0/tests/test_contour.py +54 -0
  189. unisky-0.2.0/tests/test_cutout_api.py +604 -0
  190. unisky-0.2.0/tests/test_encoding.py +75 -0
  191. unisky-0.2.0/tests/test_example_config.py +793 -0
  192. unisky-0.2.0/tests/test_footprints.py +1337 -0
  193. unisky-0.2.0/tests/test_grid.py +544 -0
  194. unisky-0.2.0/tests/test_hips.py +1012 -0
  195. unisky-0.2.0/tests/test_image.py +138 -0
  196. unisky-0.2.0/tests/test_incremental.py +2270 -0
  197. unisky-0.2.0/tests/test_legacysurvey.py +956 -0
  198. unisky-0.2.0/tests/test_lens_api.py +235 -0
  199. unisky-0.2.0/tests/test_lensing_critcurve.py +82 -0
  200. unisky-0.2.0/tests/test_lensing_delay.py +175 -0
  201. unisky-0.2.0/tests/test_lensing_findimg.py +139 -0
  202. unisky-0.2.0/tests/test_lensing_point.py +112 -0
  203. unisky-0.2.0/tests/test_lensmodel.py +149 -0
  204. unisky-0.2.0/tests/test_make_test_field.py +197 -0
  205. unisky-0.2.0/tests/test_manifest.py +116 -0
  206. unisky-0.2.0/tests/test_prerendered.py +919 -0
  207. unisky-0.2.0/tests/test_progress.py +938 -0
  208. unisky-0.2.0/tests/test_realdata_lensing.py +419 -0
  209. unisky-0.2.0/tests/test_region.py +82 -0
  210. unisky-0.2.0/tests/test_remote.py +1225 -0
  211. unisky-0.2.0/tests/test_server_app.py +671 -0
  212. unisky-0.2.0/tests/test_static_assets.py +172 -0
  213. unisky-0.2.0/tests/test_tiler.py +560 -0
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .pytest_cache/
4
+ dist/
5
+ build*/
6
+ node_modules/
7
+ frontend/.vite/
8
+ .e2e-build/
9
+ .DS_Store
10
+ .venv/
11
+ tests/fixtures/rxcj2211_mini/
12
+ frontend/test-results/
13
+ frontend/playwright-report/
14
+ # page snapshots the Playwright MCP server drops beside the cwd
15
+ .playwright-mcp/
16
+ .history
17
+
18
+ # wrangler caches the Cloudflare account it authenticated as; never publishable
19
+ .wrangler/
20
+ # agent worktrees: a whole second copy of the tree, never publishable
21
+ .claude/worktrees/
@@ -0,0 +1,386 @@
1
+ # Changelog
2
+
3
+ All notable changes to UniSky are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [0.2.0] - 2026-08-23
7
+
8
+ ### Changed
9
+
10
+ - **The project is now called UniSky.** It was AnyMap; that name is taken on
11
+ PyPI. The rename is complete rather than aliased — there is no compatibility
12
+ shim anywhere — so an existing checkout, cache and built site each need one
13
+ action:
14
+
15
+ | was | is |
16
+ |---|---|
17
+ | `pip install anymap`, `import anymap` | `pip install unisky`, `import unisky` |
18
+ | `anymap build` / `serve` / `add` | `unisky build` / `serve` / `add` |
19
+ | `$ANYMAP_CACHE`, `$ANYMAP_OFFLINE`, `$ANYMAP_SCRATCH` | `$UNISKY_CACHE`, `$UNISKY_OFFLINE`, `$UNISKY_SCRATCH` |
20
+ | `manifest.json` key `anymap_version` | `unisky_version` |
21
+ | catalog overlay marker `"AnyMapCatalog"` | `"UniSkyCatalog"` |
22
+ | `localStorage` keys `anymap.*` | `unisky.*` |
23
+
24
+ - **Reinstall the console script.** The old `anymap` entry point belongs to
25
+ the old distribution: `pip uninstall -y anymap && pip install -e ".[dev]"`.
26
+ Without `-y`, `pip uninstall` stops to ask, and outside an interactive
27
+ terminal it dies on the prompt and the `&&` never runs.
28
+ - **MERGE the download cache — do not let it be re-fetched, and do not
29
+ `mv` it.** It holds real archive downloads (DESI cutouts, HiPS tiles, TAP
30
+ answers), and the code does not look at the old path:
31
+
32
+ ```bash
33
+ # keeps whichever copy of a file is newer, and says what it moved;
34
+ # correct whether or not ~/.cache/unisky already exists
35
+ rsync -a --update --itemize-changes ~/.cache/anymap/ ~/.cache/unisky/
36
+ ```
37
+
38
+ `mv ~/.cache/anymap ~/.cache/unisky` — which earlier drafts of this entry
39
+ and of the README recommended — is correct only while the destination
40
+ does not exist. Once it does, and it does from the first `unisky build`
41
+ onwards, `mv` nests the old cache at `~/.cache/unisky/anymap/`, where
42
+ `cache_root()` never looks: it strands exactly the data it was meant to
43
+ save, and exits 0. Both trailing slashes above are load-bearing.
44
+
45
+ Nothing inside it changes; the cache key is the canonical request URL.
46
+ See [docs/remote-data.md](docs/remote-data.md).
47
+ - **Rebuild built sites.** `manifest.json` carries `unisky_version` and
48
+ nothing reads the old key, so `unisky serve` and `unisky add` refuse a
49
+ pre-rename site with a message that names the rebuild, and so does the
50
+ viewer. Rebuild with `unisky build <config.yaml> -o <site>`; configs
51
+ themselves are unchanged apart from any `$ANYMAP_*` they mention.
52
+ - Saved viewer state in the browser (theme, window positions) lives under
53
+ renamed `localStorage` keys and starts from the defaults once.
54
+
55
+ - **A broken config is now a message for a person, not a traceback.** A YAML
56
+ syntax error is reported with the line, the column, the offending source
57
+ line and a caret under it. A config that parses but does not validate is
58
+ refused with **every** problem at once, each carrying the line it is on, the
59
+ field path, the layer's `id`, and the nearest known name when there is one:
60
+
61
+ ```
62
+ Error: config.yaml is not a valid UniSky config; 2 problems.
63
+
64
+ the config file:
65
+ /home/you/fields/config.yaml
66
+
67
+ what to do:
68
+ - line 31, layers[3].channels.g (layer 'rgb'): term 'half*f200w' has
69
+ no numeric weight: 'half' is not a number. Write it as
70
+ '<weight>*<layer id>', for example '0.5*f322w2'
71
+ - line 32, layers[3].lupton.q (layer 'rgb'): unknown key 'q' - did you
72
+ mean 'Q'?
73
+ ```
74
+
75
+ A file the config names but that cannot be read is caught the same way, in
76
+ one pre-flight pass before any work starts — every missing path at once,
77
+ resolved and printed in full, with nothing built and nothing written.
78
+ `--traceback` still prints the Python one underneath, and a failure that
79
+ looks like a bug in UniSky prints one anyway.
80
+
81
+ - **Config errors, unreadable files and archive failures are all laid out the
82
+ same way**: what happened, then the file, URL or directory it happened to,
83
+ then what to do about it — hard-wrapped to a fixed 72 columns (not
84
+ `$COLUMNS`, so the text reads the same wherever it lands), except that a
85
+ path, URL or command is left on its own line however long it is, because
86
+ wrapping one breaks copy-paste. It replaces the 1096-character single line
87
+ one of them used to be. A test reads every refusal in the package out of the
88
+ source and holds it to that layout, so one written tomorrow is covered the
89
+ moment it is written.
90
+
91
+ ### Added
92
+
93
+ - **`unisky add` and `unisky remove`: one more layer into a site that is
94
+ already built, and one back out.** A full-field build is minutes and
95
+ gigabytes, and a region file that turns up afterwards should not cost that
96
+ twice.
97
+
98
+ ```bash
99
+ unisky add config.yaml -o build/ lrd_msa # build ONE more layer in place
100
+ unisky remove -o build/ lrd_msa # ...and take it back out
101
+ ```
102
+
103
+ `add` builds only the layers you name, splices them into `manifest.json` in
104
+ **config order**, and leaves every other layer's tiles byte for byte as they
105
+ were. `remove` is the inverse and takes **no config at all**: what a layer
106
+ owns, and which composites are rendered from it, are read from the site's
107
+ own manifest — so a layer can leave the site before, after, or instead of
108
+ leaving the config. Both refuse rather than corrupt a site (a config that no
109
+ longer describes its display grid; a layer that misses the grid; a channel
110
+ whose composite would be left showing pixels it no longer has; the last
111
+ image layer), and `--dry-run` runs every check without writing a byte.
112
+
113
+ **The display grid never moves.** It is derived once, when the site is
114
+ built, from the union of every image layer's footprint, and every tile on
115
+ disk is addressed by it — so `add` reads the grid out of `manifest.json` and
116
+ reuses it exactly rather than re-derive it, and `remove` leaves it as wide
117
+ as it was even when the widest layer goes. Only `unisky build` derives a
118
+ grid. Remove a layer, add it straight back, and the site is the same bytes.
119
+ See [docs/incremental.md](docs/incremental.md).
120
+
121
+ - **A site now says what its DEPLOYMENT can offer, so one build can feed two.**
122
+ `manifest.json` grows `serve.cutout` and `serve.lensing`, and `unisky build`
123
+ grows three flags that set them without the config being edited between the
124
+ full internal site and the copy going somewhere poorer:
125
+
126
+ ```bash
127
+ unisky build config.yaml -o site/ # both on: the full server
128
+ unisky build config.yaml -o site/ --no-cutout # has the folder, not the data
129
+ unisky build config.yaml -o site/ --static # --no-cutout --no-lensing
130
+ ```
131
+
132
+ **Two switches and not one**, because the two server-side features do not
133
+ need the same machine. A cutout re-opens the ORIGINAL FITS at request time,
134
+ by the absolute path in the layer's `native` block, so it needs the dataset
135
+ on the serving host — often terabytes that were never in the build folder.
136
+ The lensing endpoints read only the `.npy` grids the build already wrote
137
+ *inside* the folder, so they need a Python process and nothing else. A small
138
+ server holding the build and none of the data behind it can therefore do one
139
+ and not the other, and one switch could not have said that. Both default to
140
+ **true**, and a manifest carrying neither key — every site built before this
141
+ round — keeps meaning "everything, as before".
142
+
143
+ The flags are **negative only**: the config's `serve:` block is the ceiling,
144
+ and a build of it may be poorer, never richer.
145
+
146
+ - **`unisky capabilities <site>`: the two booleans, on a site already built.**
147
+
148
+ ```bash
149
+ unisky capabilities site/ # with no flags: what does it offer?
150
+ unisky capabilities site/ --static # about to be uploaded to object storage
151
+ unisky capabilities site/ --lensing # ...and back up again, a month later
152
+ ```
153
+
154
+ No config, no rebuild, and not one tile read or written: `manifest.json` is
155
+ republished by the same same-directory temp plus atomic rename that `add` and
156
+ `remove` use, so this is as cheap on a 300 GB site as on an empty one and an
157
+ interrupted run leaves the site exactly as it was. A new subcommand rather
158
+ than a flag, because every existing command needs something this does not (a
159
+ config, forty minutes, artifacts to delete, a process that outlives nothing),
160
+ and a **noun** because with no flags it changes nothing and reports what the
161
+ site offers — the question anyone asks before pushing a folder somewhere.
162
+
163
+ Here, and only here, a capability may go back **up**: this is the site's
164
+ declaration being written rather than read, and the way back from a flag
165
+ typed at the wrong directory must not be a second forty-minute build. Putting
166
+ cutouts back checks that the FITS this site names are readable here and
167
+ **warns rather than refuses** — a deployment folder is usually prepared
168
+ somewhere other than where it will be served.
169
+
170
+ A `--static` build still ships `lens/**` on purpose: it is what lets
171
+ `--lensing` turn lensing back on later without rebuilding, and it makes a
172
+ flipped site **byte-identical** to one rebuilt with the same shape, manifest
173
+ included.
174
+
175
+ - **Everything else agrees with the manifest, and only ever downward.**
176
+ `unisky serve` mounts each API router only when its flag is true; what takes
177
+ a router's place is a short **404** naming the flag — 404 and not 501 or 503,
178
+ because the object-storage copy of the same folder answers 404 and nothing
179
+ can change that, so one status means one thing and a client needs one code
180
+ path instead of two. `unisky serve --no-cutout` / `--no-lensing` / `--static`
181
+ lower a full build for one process and cannot raise one — there is no
182
+ `--cutout` to type — and a lowered process serves a lowered `manifest.json`
183
+ too, since the viewer decides what to offer from that document. `unisky add`
184
+ re-derives the block from the config like everything else and then **floors
185
+ it** with what the site already declares, saying so when the floor bites: a
186
+ rebuilt layer is not a reason for the FITS to have arrived on the server.
187
+
188
+ - **In the viewer, a feature the deployment cannot serve is ABSENT, not greyed
189
+ out.** `src/capabilities.ts` is the one place the two booleans are read, and
190
+ only a literal `false` takes a feature away — a missing block, a null, a
191
+ string left by a hand-edit all read as "available", because that is what they
192
+ have always meant. What goes with it is every way to ask: no panel, no dock
193
+ button, no remembered "was open last visit" (all three come from one
194
+ `registerWindow` that is never called), no menu item, and no accelerator in
195
+ the key map — *unbound* rather than bound to a no-op, so the letter does
196
+ nothing rather than quietly doing nothing. `api.ts` refuses on its own too,
197
+ before a request is built, because unreachable has to be a property of the
198
+ module and not of one wiring. The Tools menu drops the separators that had
199
+ been holding the removed items' places rather than opening on a rule with
200
+ nothing above it.
201
+
202
+ The keys describe the **machine, not the science**: `lensing: false` does not
203
+ mean the field has no lens model. Its kappa and mu tiles are ordinary picture
204
+ tiles and keep being drawn, the layer census still counts the model, and the
205
+ About window says the same words on every site. What goes is every way to ask
206
+ a question this deployment cannot answer.
207
+
208
+ Both deployment stories — a collaboration server, and Cloudflare with the
209
+ tiles in R2 and `/api/*` on one hostname through a Tunnel — are now in
210
+ [docs/deploy.md](docs/deploy.md), which has been run end to end.
211
+
212
+ - **`unisky build` builds several layers at once.** Layers are independent, so
213
+ images, catalogs, contours, regions and lens models run in parallel
214
+ processes, and whatever the layers cannot use goes to threads *inside* a
215
+ layer, parallelizing the reprojection bands and the tile encoding. `-j N` is
216
+ a total core budget (`-j 1` strictly serial, `-j 0` every core); the default
217
+ is the smaller of 75% of the cores and as many concurrent layers as fit in
218
+ half of RAM, so it stays sane on a 64-core box and on a laptop alike.
219
+ **The output does not depend on `-j`** — every choice writes byte-identical
220
+ tiles and the same manifest, whose layers stay in config order however the
221
+ build finishes, and the test suite builds the committed real-data fixture
222
+ serially and in parallel and compares every file's hash.
223
+
224
+ - **A build shows what it is doing while it does it**: one in-place bar, plus
225
+ one line per layer *in flight* with its id, its stage, its progress within
226
+ that stage and how long it has been running; finished layers scroll off as
227
+ ordinary log lines, so the scrollback is still a build log. Progress is
228
+ **work-weighted**, not layer-counted — a full-resolution JWST layer writes
229
+ ~1000 tiles where a native-scale-capped IRAC layer writes ~80, so each
230
+ layer's cost is estimated up front from the decimated grid it will actually
231
+ tile — and the ETA is **withheld** until a tenth of that weighted work has
232
+ been measured, because dividing by a tiny fraction multiplies its error by
233
+ `(1-f)/f`. It degrades to one plain line per layer whenever stdout is not a
234
+ terminal or `TERM=dumb`; `--no-progress` forces that on a terminal too, and
235
+ `NO_COLOR` is honoured separately (the live display stays, the colour goes).
236
+ A Ctrl-C or a failing layer erases the frame and restores the cursor without
237
+ swallowing the error, and a build with the display on writes byte-identical
238
+ files to one with it off.
239
+
240
+ - **Composite recipes in YAML gained the two things the viewer's RGB panel
241
+ already had.** A channel may be a **weighted sum** of image layers, and the
242
+ three channels may be combined by **Lupton**'s rule instead of one at a
243
+ time:
244
+
245
+ ```yaml
246
+ channels: {r: f322w2, g: 0.5*f322w2 + 0.5*f150w2, b: f150w2}
247
+ lupton: {stretch: 5, Q: 8}
248
+ ```
249
+
250
+ The sum is the two-filter trick — with only two wide bands there is no
251
+ middle filter for green, so green is synthesized — and weights are
252
+ relative, divided by their sum before mixing, so `1*a + 1*b + 1*c` is the
253
+ average of the three rather than three times the brightness. Lupton scales
254
+ all three channels by one common asinh factor derived from their mean, so a
255
+ bright core keeps the colour of its spectrum instead of saturating to white;
256
+ `lupton: {}` takes astropy's own defaults. Both are written exactly as the
257
+ RGB panel writes them, so a composite designed in the browser can be pasted
258
+ into a config and built — and `tests/test_composite.py` runs the viewer's
259
+ **own** TypeScript against the builder, whole pipeline, holding the two to
260
+ one display level. `examples/rxcj2211.yaml` carries a weighted composite for
261
+ the NIRCam wide pair. See [docs/composites.md](docs/composites.md).
262
+
263
+ - **A stale viewer bundle can no longer be committed unnoticed.**
264
+ `src/unisky/static/` is a build product that lives in git — it is what
265
+ `unisky build` copies into every site and what the wheel ships — so a
266
+ frontend change that was not rebuilt reached every user as the old viewer.
267
+ That had happened three times, and each time a human noticed the wrong thing
268
+ on screen. `just build-fe` now writes `bundle-source.json` beside the
269
+ bundle, recording the SHA-256 of every file vite read and every file it
270
+ wrote; `just check-fe-fresh` — and the ordinary `pytest` run — recompute
271
+ both offline in well under a second and fail naming the files that moved and
272
+ the one command that fixes it. `vite build` empties the directory on its way
273
+ through, so a build that skips the stamping step leaves no stamp at all, and
274
+ no stamp fails loudly rather than passing quietly. See
275
+ [docs/frontend-bundle.md](docs/frontend-bundle.md).
276
+
277
+ - Gravitational **time delays**: a lens model may now carry the lensing
278
+ potential (`maps.psi`, arcsec², glafic's `_pot` dump). With it,
279
+ `/api/lens/findimg` reports `delay_days` for every predicted counter-image
280
+ — the arrival time relative to the first image of that source — and the
281
+ lensing panel labels each image with it. `LensModel` grows
282
+ `fermat_potential()`, `time_delay_distance()` and `delay_days()`;
283
+ `/api/lens/models` and the manifest report `has_potential`.
284
+ - The RGB window can now **build a weighted channel**: "+ band" adds a term
285
+ to a channel, each term gets a weight, and the channel becomes the weighted
286
+ sum the compositor already supported (previously reachable only by editing
287
+ the permalink by hand).
288
+
289
+ ### Fixed
290
+
291
+ - **`docs/deploy.md` opened by naming two answers to a question that now has
292
+ three.** "The cutout API needs the original FITS files, so either build on
293
+ the server or mount the dataset at the same absolute path" was the whole
294
+ section, written before there was a third answer — publish the site without
295
+ cutouts and let the deployment say so. The section now names all three, and
296
+ the document goes on to the deployment the third one exists for. Nothing in
297
+ `docs/` or the README had mentioned `--no-cutout`, `--static`,
298
+ `serve.cutout`, `serve.lensing` or `unisky capabilities` at all.
299
+ - **The README's figure for what an `mv` of the cache would strand was one
300
+ count too loose.** It said "68 MB in 68 files"; on the machine it describes
301
+ the number is 66 files, and the line now carries the one-line `diff` that
302
+ produces it, so the next person measures rather than trusts it.
303
+ - **The README told you to destroy the cache it told you to keep.** Its
304
+ upgrade step was `mv ~/.cache/anymap ~/.cache/unisky`, which is right only
305
+ while the destination does not exist — and it exists from the first
306
+ `unisky build` onwards, after which `mv` nests the old cache at
307
+ `~/.cache/unisky/anymap/`, where `cache_root()` never looks. The command
308
+ stranded exactly the downloads it existed to preserve, and exited 0. It is
309
+ now an `rsync -a --update` merge that keeps whichever copy of a file is
310
+ newer, works whether or not the destination exists, prints what it moved,
311
+ and is followed by a `diff` that names anything left behind. Corrected in
312
+ the README, in this entry above, and in `docs/remote-data.md`.
313
+ - **Three more things the README claimed that a real machine refuted.**
314
+ `python3 -m venv .venv` picks up Python 3.9 on stock macOS, below the
315
+ `>=3.10` two lines above it, and the failure that follows is pip 21.2.4
316
+ blaming `setup.py` — so the interpreter is now named and checked, and pip
317
+ upgraded, before anything is installed. `UNISKY_RXCJ2211=… just test` was
318
+ described as running the whole real-data set, which it cannot promise: it
319
+ says where the dataset is, and each test decides by opening a file. And a
320
+ grid-clash refusal was quoted verbatim from a version that no longer
321
+ existed; the quote is replaced by the `--dry-run` that prints the current
322
+ one. Running the rest of the file end to end from a fresh clone also turned
323
+ up `just e2e` needing `npx playwright install chromium`, which `just setup`
324
+ does not do, and `pip uninstall anymap` needing `-y` to survive a
325
+ non-interactive shell.
326
+
327
+ - **The viewer's refusal of a pre-rename site expired after 60 seconds**,
328
+ leaving a blank black page with nothing in the console either. The three
329
+ toasts raised when `boot()` gives up — `manifest.json` unreachable, refused
330
+ for a missing `unisky_version`, or a throw on the way up — are not notices
331
+ on top of a working map; they are the whole page, so they now wait to be
332
+ dismissed instead of timing out. Ordinary toasts still fade after 5 s.
333
+ - Restoring the lensing panel from its dock button drew no critical curve —
334
+ only the menu-bar route refreshed it. `WindowSpec` grows an `onShow` hook
335
+ that fires for every route back on screen.
336
+ - **Coverage footprints drew as near-opaque slabs**, hiding the image they
337
+ are meant to be read against. The opacity slider's generic vector path
338
+ stamped `fillOpacity: opacity * 0.8` on every child of every overlay, so a
339
+ footprint's deliberately faint 0.07 fill (and a region's 0.6) was replaced
340
+ rather than scaled the moment the layer was enabled. It now scales each
341
+ overlay's OWN fill, remembered once so repeated slider ticks cannot
342
+ compound. Catalogs were already immune — they override `setOpacity` to
343
+ defend against exactly this — which is why it surfaced only with the new
344
+ `footprint` layers.
345
+ - **Popups rounded coordinates to 4 significant digits**, so a source at
346
+ RA 332.952929 was shown as `333` — 169 arcsec out — and Dec −3.830160 as
347
+ `-3.83`. Coordinate columns are now printed to a fixed 6 decimals (3.6 mas)
348
+ with trailing zeros dropped; every other column keeps the 4-significant-
349
+ digit rule. Affects built-in and uploaded catalogues alike.
350
+ - **A 64-bit catalogue id read `48420000000000000` for every source.** Two
351
+ independent bugs produced that one number. The popup printed an integer
352
+ verbatim only below 1e7 and sent everything above through a 4-significant-
353
+ digit rounding, so a DESI `ls_id` near 4.84e16 kept four digits and gained
354
+ sixteen zeros — and its neighbours, sharing those four digits, became the
355
+ same id. An identifier is a name that happens to be spelled in digits, so an
356
+ integer is now printed in full at any magnitude. Fixing the formatter alone
357
+ was not enough: 4.8e16 is five times past 2^53, so an id written as a JSON
358
+ *number* is already rounded by the time `JSON.parse` returns. The builder now
359
+ writes an integer outside JavaScript's safe range as a JSON **string** and
360
+ the viewer renders it as one; ordinary small integers stay numbers, so a
361
+ short id still sorts and compares numerically. Catalogues dropped onto the
362
+ map in the browser follow the same rule.
363
+
364
+ ## [0.1.0] - 2026-08-20
365
+
366
+ First release.
367
+
368
+ ### Added
369
+
370
+ - `unisky build`: config-driven builder producing a self-contained static
371
+ site — quantized 16-bit PNG data tiles with client-side stretch, composite
372
+ RGB tiles, catalog/contour/region GeoJSON overlays, and lens-model grids.
373
+ - `unisky serve`: FastAPI server for the built site plus the science API:
374
+ native-grid FITS cutouts (`/api/cutout`) and strong-lensing endpoints
375
+ (`/api/lens/critcurve`, `/api/lens/mu`, `/api/lens/findimg`,
376
+ `/api/lens/models`); optional basic auth and CORS.
377
+ - Leaflet + WebGL frontend with grouped layers, stretch/colormap panel,
378
+ catalog popups, coordinate readout and goto box, cutout tool, lensing
379
+ panel with z_s slider, and URL-hash permalinks.
380
+ - RXCJ2211-0350 production example config (`examples/rxcj2211.yaml`) covering
381
+ JWST NIRCam, HST RELICS, ALMA continuum + contours, Spitzer IRAC, merged
382
+ photometry with EAZY photo-z join, DS9 regions, and the glafic v0.91 lens
383
+ model.
384
+ - Real-data fixture subsampler (`scripts/make_test_field.py`), a committed
385
+ sub-3 MB tiny field, and a six-spec Playwright E2E suite.
386
+ - Deployment guide (systemd + nginx + basic auth) and `just` dev targets.
unisky-0.2.0/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Mingyu Li
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.