Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
63479bd
Initial plan
Copilot Mar 3, 2026
b5bb50f
Add CosmoSIS GUI: Flask/WebSocket backend, Pipeline canvas, tab navig…
Copilot Mar 3, 2026
9a629d6
Vertical pipeline: top-down layout, left-arcing bezier arcs, deselect…
Copilot Mar 3, 2026
86e0185
Module categories, canvas drag-to-reorder, fix arc source tracing
Copilot Mar 3, 2026
f627d66
Module library: categories collapsed by default; fix duplicate handle…
Copilot Mar 4, 2026
93ff234
Visual tweaks, expandable sections, and params form
Copilot Mar 4, 2026
e6cfdf0
Server-side directory scan, cosmosis Inifile integration, Open Pipeli…
Copilot Mar 4, 2026
d357463
Rewrite as Electron application with native OS file dialogs
Copilot Mar 6, 2026
dfa6798
Change port to 8080
joezuntz Mar 6, 2026
253819c
Pipeline canvas visual improvements: arrow routing, connector anchors…
Copilot Mar 6, 2026
1db1420
Replace Flask/Socket.IO backend with Electron IPC + Python stdin/stdo…
Copilot Mar 9, 2026
ec87d64
add os.chdir so we can load ini files with relative paths
joezuntz Mar 9, 2026
4a77a56
Backend class, param updates, Prepare Pipeline + Run Likelihood buttons
Copilot Mar 9, 2026
bef82a5
Fix JSON noise, gray-out buttons, move Pipeline section, fix output s…
Copilot Mar 9, 2026
bf1e031
Fix module output splitting: use fd-level capture to catch C-extensio…
Copilot Mar 9, 2026
74966cc
After run_likelihood: show actual DataBlock I/O per module, change mo…
Copilot Mar 9, 2026
485c8af
Fix syntax error: remove stray closing brace in app.js that caused un…
Copilot Mar 9, 2026
c9060c2
change how dtype is determined
joezuntz Mar 10, 2026
fa2bce5
Fix str types
joezuntz Mar 10, 2026
514a9b9
feat: Results tab, pipeline status indicators, CWD auto-scan
Copilot Mar 10, 2026
2364d34
fix: Results tab fills main area and section expand works correctly
Copilot Mar 10, 2026
434c485
fix: unpack (section, name) pairs from DataBlock.keys() so Results ta…
Copilot Mar 10, 2026
c6d1558
fix numpy thing
joezuntz Mar 10, 2026
013a29d
Merge branch 'copilot/add-basic-ui-infrastructure' of https://github.…
joezuntz Mar 10, 2026
e4a54de
feat: Results tab two-column layout with Plotly, log-scale/X-Y select…
Copilot Mar 10, 2026
682ece6
fix: centre-align pipeline modules and eliminate run_likelihood hang
Copilot Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
*.egg-info/
dist/
build/
.eggs/
*.egg
venv/
.venv/
env/
*.env

# Flask
instance/
.flask_session/

# Electron / Node
node_modules/
package-lock.json
out/
dist-electron/

# IDE / OS
.DS_Store
.idea/
.vscode/
*.swp
*~
Thumbs.db
79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,78 @@
# gui-v2
# CosmoSIS GUI v2

A graphical pipeline editor for [CosmoSIS](https://cosmosis.readthedocs.io/).
Built with **Electron** (frontend shell) and **Python** (backend logic via stdin/stdout IPC).

---

## Architecture

The GUI uses a clean Electron-native architecture — no HTTP server required:

```
Electron main process
├─ loads templates/index.html via loadFile() (file:// — no port)
├─ spawns worker.py as a child process
└─ bridges IPC calls: renderer ↔ ipcMain ↔ worker.py stdin/stdout
```

All Python I/O (scanning module libraries, parsing pipeline INI files) is
handled by `worker.py` — a simple JSON-RPC worker that reads requests from
stdin and writes responses to stdout. This replaces the previous
Flask/Socket.IO HTTP server approach, which had several drawbacks:

| | Old (Flask) | New (IPC) |
|-|-------------|-----------|
| Port conflicts | Yes (macOS blocked 5000) | None |
| Startup delay | 200 ms polling loop | Instant |
| Extra Python deps | Flask, flask-socketio, flask-cors, simple-websocket | None |
| Security surface | localhost HTTP socket | Subprocess pipe only |

---

## Requirements

| Tool | Version |
|------|---------|
| Python | ≥ 3.9 |
| Node.js | ≥ 18 |
| CosmoSIS | ≥ 3.22 |

---

## Getting started

### 1 — Install Python dependencies

```bash
pip install -r requirements.txt
```

### 2 — Install Node.js / Electron dependencies

```bash
npm install
```

### 3 — Run the application

```bash
npm start
```

Electron will spawn `worker.py` as a subprocess and open the window immediately.

---

## Usage

* **Open Library** — click the button in the left sidebar and choose the root
of a CosmoSIS standard library directory. The Python worker recursively
scans for `module.yaml` files and populates the module library panel.

* **Open Pipeline** — click the button and choose a CosmoSIS `.ini` pipeline
file. The worker parses it with `Inifile`, resolves each module against
the scanned library, and populates the pipeline canvas.

* **Drag modules** from the library panel onto the canvas to build a custom
pipeline.
183 changes: 183 additions & 0 deletions inifile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""Bundled subset of the CosmoSIS Inifile class (cosmosis.runtime.config).

This module is used as a fallback when the full ``cosmosis`` package is not
installed (e.g. because its Fortran/C extensions cannot be compiled in the
current environment). When ``cosmosis`` *is* available the classes are
imported from there instead, so the behaviour is identical to the real package.

Only the two pure-Python classes required by the GUI are included here:
``IncludingConfigParser`` and ``Inifile``. The source is verbatim from the
``cosmosis`` package (MIT licence).
"""

try:
# Prefer the real cosmosis package when available.
from cosmosis.runtime.config import Inifile, IncludingConfigParser # noqa: F401
except Exception: # ImportError, or compilation failures on some platforms
# ── Verbatim copy from cosmosis/runtime/config.py ────────────────────
import collections
import configparser
import io
import os

class IncludingConfigParser(configparser.ConfigParser):
"""Extension of ConfigParser that supports ``%include filename.ini``."""

def __init__(self, defaults=None, print_include_messages=True,
no_expand_vars=False):
self.no_expand_vars = no_expand_vars
configparser.ConfigParser.__init__(
self,
defaults=defaults,
dict_type=collections.OrderedDict,
strict=False,
inline_comment_prefixes=(";", "#"),
)
self.print_include_messages = print_include_messages

def _read(self, fp, fpname):
s = io.StringIO()
for line in fp:
if not self.no_expand_vars:
line = os.path.expandvars(line)
if line.lower().startswith("%include"):
_, filename = line.split()
filename = filename.strip('"').strip("'")
if self.print_include_messages:
print(f"Reading included ini file: {filename}")
if not os.path.exists(filename):
raise ValueError(
f"Tried to include non-existent file {filename}"
)
sub_ini = self.__class__(filename)
sub_ini.write(s)
else:
s.write(line)
s.seek(0)
return super()._read(s, fpname)

class CosmosisConfigurationError(configparser.Error):
pass

class Inifile(IncludingConfigParser):
"""A ``(section, name) → value`` mapping backed by a ``.ini`` file."""

def __init__(self, filename, defaults=None, override=None,
print_include_messages=True, no_expand_vars=False):
IncludingConfigParser.__init__(
self,
defaults=defaults,
print_include_messages=print_include_messages,
no_expand_vars=no_expand_vars,
)
if isinstance(filename, dict):
for section, values in filename.items():
self.add_section(section)
for key, value in values.items():
self.set(section, key, str(value))
elif isinstance(filename, Inifile):
s = io.StringIO()
filename.write(s)
s.seek(0)
self.read_file(s)
elif hasattr(filename, "read"):
self.read_file(filename)
elif filename is not None:
if isinstance(filename, str) and not os.path.exists(filename):
raise IOError(
f"Unable to open configuration file `{filename}'"
)
self.read(filename)

if override:
for section, name in override:
if section == "DEFAULT":
self._defaults[name] = override[(section, name)]
else:
if not self.has_section(section):
self.add_section(section)
self.set(section, name, override[(section, name)])

def get(self, section, option, raw=False, vars=None,
fallback=configparser._UNSET):
try:
return IncludingConfigParser.get(
self, section, option, raw=raw, vars=vars,
fallback=fallback
)
except (configparser.NoSectionError,
configparser.NoOptionError) as exc:
if fallback is configparser._UNSET:
raise CosmosisConfigurationError(
f"CosmoSIS looked for '{option}' in '[{section}]' "
"but it was not in the ini file"
) from exc
return fallback

def __getitem__(self, key):
section, option = key
return self.get(section, option)

def __setitem__(self, key, value):
section, option = key
self.set(section, option, str(value))

def __iter__(self):
return (
((section, name), value)
for section in self.sections()
for name, value in self.items(section)
)

def getboolean(self, section, option, raw=False, vars=None,
fallback=configparser._UNSET):
try:
return IncludingConfigParser.getboolean(
self, section, option, raw=raw, vars=vars,
fallback=fallback
)
except ValueError:
value = self.get(section, option).lower()
if value in ("y", "yes", "t", "true"):
return True
if value in ("n", "no", "f", "false"):
return False
raise ValueError(
f"Cannot parse '{value}' as boolean "
f"for [{section}] {option}"
)
except (configparser.NoSectionError,
configparser.NoOptionError) as exc:
if fallback is configparser._UNSET:
raise CosmosisConfigurationError(
f"CosmoSIS looked for boolean '{option}' in "
f"'[{section}]'"
) from exc
return fallback

def gettyped(self, section, name):
"""Best-guess the type of a parameter."""
import re
value = IncludingConfigParser.get(self, section, name)
value = value.strip()
if not value:
return None
m = re.match(r"^(['\"])(.*?)\1$", value)
if m is not None:
return m.group(2)
value_list = value.split()
try:
parsed = [int(s) for s in value_list]
return parsed[0] if len(parsed) == 1 else parsed
except ValueError:
pass
try:
parsed = [float(s) for s in value_list]
return parsed[0] if len(parsed) == 1 else parsed
except ValueError:
pass
try:
return self.getboolean(section, name)
except ValueError:
pass
return value
Loading