Skip to main content
Glama
README.md
# dsh-origin-thesis

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Platform: Windows](https://img.shields.io/badge/Platform-Windows-0078d4.svg)](#requirements)
[![Python: 3.9](https://img.shields.io/badge/Python-3.9-3776ab.svg)](#requirements)
[![Origin: 2018+](https://img.shields.io/badge/Origin-2018%2B-orange.svg)](#requirements)
[![DSH bundle](https://img.shields.io/badge/DSH-bundle-4b5563.svg)](cordis.patch.yml)
[![tools: 12](https://img.shields.io/badge/MCP%20tools-12-6f42c1.svg)](#tools)

**Format OriginLab Origin figures to a fixed house style from inside DeepSeek Harness.**
One command turns a whole `.opju` project into publication-ready figures: per-layer
grid layout, unified fonts and sizes, render-measured axis-title placement,
overlap resolution, panel labels, and 600-dpi export with corrected dpi metadata.

Built for a PhD-thesis workflow where dozens of Origin graphs must end up looking
identical, and rebuilt as a DSH bundle so the formatting runs from chat instead of
a command line.

```
you:  把这份项目按论文格式重排,保留我手摆的图例
      → thesis_format_project(project="...\project.opju",
                              profile="thesis", keep_legend_pos=true)
      → 13 figures reformatted, <name>_thesis.opju + <name>_thesis_figures/*.png
```

---

## Requirements

| | |
|---|---|
| **OS** | **Windows only.** Origin's automation interface is Windows COM; there is no macOS/Linux path. |
| **Origin** | A locally installed, licensed copy of **Origin / OriginPro 2018 or newer**. Tested on OriginPro 2026b and Origin 2021. **Not bundled with this plugin** — you must own an Origin licence. |
| **Python** | An interpreter with `originpro`, `numpy` and `Pillow` installed. ⚠️ `originpro` currently ships wheels **only up to Python 3.9**, so this is normally a 3.9 environment (e.g. a conda env named `origin`). |
| **DSH** | DeepSeek Harness with the `@deepseek-ai/dsh-mcp-client` plugin available (any DSH profile that can mount an MCP client). |

```bat
:: create the interpreter this plugin will drive Origin with
conda create -n origin python=3.9 -y
conda activate origin
pip install originpro numpy Pillow
```

> The plugin itself needs **no `mcp` package**: `thesis_mcp_server.py` implements the
> MCP stdio JSON-RPC loop directly, so it runs on Python 3.9 or 3.10 alike.

### Why there is a `.cmd` launcher

`cordis.patch.yml` names **[`start-thesis-mcp.cmd`](start-thesis-mcp.cmd)** rather than
`python.exe`. That is deliberate, and it is what makes the package location-independent.

DSH evaluates the `!!js` expressions in its patch files in a sandbox where
**`__dirname`, `__filename` and `require` do not exist** — only `process` is reliably
available. Measured, not assumed:

```
mcp-origin-thesis (@deepseek-ai/dsh-mcp-client): ReferenceError: __dirname is not defined
```

So the patch cannot compute its own package directory in JavaScript, and any
`!!js require('node:path').join(__dirname, …)` construction fails at startup. A Windows
batch file, by contrast, gets its own directory from the built-in `%~dp0` expansion with
no JavaScript involved. The launcher therefore:

1. locates `thesis_mcp_server.py` next to itself via `%~dp0`;
2. picks an interpreter — `DSH_THESIS_PYTHON`, else a conda env named `origin` in the
   usual places, else `python` on `PATH`;
3. forwards all arguments (`-u -X utf8` plus anything DSH passes) and runs the server.

The patch only has to name the launcher, so **the package can live anywhere** — the
default `~/dsh-vendor/dsh-origin-thesis` is just a convention. If you install it
elsewhere, edit the two `args`/`cwd` values in `cordis.patch.yml` accordingly.

---

## Install

### 1. Get the files

```bat
git clone https://github.com/xinchun2018/dsh-origin-thesis.git "%USERPROFILE%\dsh-vendor\dsh-origin-thesis"
```

Everything needed at runtime — the server, the style profiles, **and the plotting
scripts** — lives in that one directory.

### 2. Wire it into a DSH profile

Clone into the location DSH expects (or point the profile at wherever you put it),
then link it into the profile the way any out-of-tree bundle is linked:

```bat
cd "%USERPROFILE%\.dsh\profiles\web"
pnpm add link:%USERPROFILE%\dsh-vendor\dsh-origin-thesis
```

and make sure the bundle is listed in that profile's `package.json`:

```json
"dsh": {
  "profile": {
    "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app",
                "dsh-origin-thesis"]
  }
}
```

The bundle's own `cordis.patch.yml` then registers the loader entry
(`mcp-origin-thesis` → `@deepseek-ai/dsh-mcp-client`, stdio) and resolves both the
server path and the interpreter automatically — **no absolute paths to edit**.

### 3. Restart DSH

The tools appear as `mcp__origin_thesis__*`. Check the wiring with:

```
thesis_batch_dir_info()
```

It reports which interpreter was used, whether `originpro` imports, and which
copy of the plotting scripts is active.

---

## Configuration

Everything is resolved at startup; nothing is hard-coded to a machine.

| Environment variable | Default | Meaning |
|---|---|---|
| `DSH_THESIS_PYTHON` | auto-detect | Interpreter that drives Origin. Detection order: this variable → `~/miniconda3/envs/origin/python.exe` → `~/anaconda3/envs/origin/python.exe` → `C:/ProgramData/miniconda3/envs/origin/python.exe` → `python` on `PATH`. |
| `DSH_THESIS_BATCH_DIR` | the package directory | Use **your own** `origin-batch-style` checkout instead of the scripts vendored in this package. |
| `DSH_THESIS_STYLES` | `styles.json` in the package directory | Use your own style profiles file. |

### Style profiles

`styles.json` is the single source of truth for the look of the figures. **Edit the
JSON to change the format — no Python changes needed.** Three profiles ship:

| Profile | For | Page | Tick / axis-title / legend | Line |
|---|---|---|---|---|
| `thesis` | Thesis figures, re-laid-out by layer count | 1 layer 9.8594×7.3999 cm (4:3); 2 layers 16×7.4; 3–4 16×13.2; 6 16×10.4; 8 16×8.6 | 9 / 10.5 bold / 9 pt | 1.0 pt |
| `composite` | Hand-assembled multi-panel plates | 17.5 cm wide, height from the original aspect (or re-gridded) | 6.5 / 7.5 / 6.5 pt | 0.75 pt |
| `manuscript` | Journal single figures (keeps the author's hand-placed legends) | same as `thesis` | same as `thesis` | same as `thesis` |

`manuscript` is `inherits: thesis` + one override (`move_legend: false`), so editing
`thesis` also affects it. Change a value at runtime with `thesis_style_set`
(dry-run first, then `dry_run=false`; a `.bak` is written automatically).

---

## Tools

**Offline (no Origin needed, instant):**

| Tool | Purpose |
|---|---|
| `thesis_styles_list` | List profiles and their key values |
| `thesis_style_show` | Show a full profile (with inheritance resolved) |
| `thesis_style_set` | Edit `styles.json` (dry-run by default) |
| `thesis_batch_dir_info` | Report resolved interpreter / scripts / styles (troubleshooting) |

**Formatting and export:**

| Tool | Purpose |
|---|---|
| `thesis_format_project` | Format **every graph in a project** by layer count; saves `*_thesis.opju` + `*_thesis_figures/*.png` |
| `thesis_format_composite` | Format a hand-assembled multi-panel plate; `regrid=true` re-spaces rows and trims the page |
| `thesis_batch` | Point it at many projects / a directory; returns a per-project report |
| `thesis_export_project` | Export only, no restyling (multiple formats, unified width, margin cropping) |
| `thesis_verify_project` | Read-only check of fonts / sizes / defects |

**Atomic, for free composition:**

| Tool | Purpose |
|---|---|
| `thesis_format_graph` | Format **one named graph** — the rest of the project is untouched |
| `thesis_apply_style_only` | Apply style/layout only: **no save, no export** |
| `thesis_export_open` | Export from the **already-open** session (no file reopen) |

These three are what let you interleave with fine-grained editing. The plugin shares
the Origin session with any other Origin tooling, so this works:

```
① thesis_apply_style_only(project=..., graph="Graph20", keep_open=true)   # ~21 s
② origin_edit_plot / origin_edit_axis / origin_edit_legend ...             # ms
③ thesis_export_open(graph="Graph20", out_path="D:\fig\Graph20.png")       # ~0.3 s
```

Use `keep_open=true` for chaining: `op.exit()` closes the project, and the next step
would then fail to find the graph.

The **original files are never modified** — formatting works on a copy in `%TEMP%`.

---

## Why the exported figures are physically sized correctly

Origin writes **300 dpi** into image headers no matter how many pixels you asked for,
so a 600-dpi figure dropped into Word is placed at twice its intended size. This
plugin patches the header after export (PNG `pHYs`, TIFF tags 282/283, JPEG JFIF,
BMP `PelsPerMeter`) without re-encoding, so the declared physical size equals the page
size set in Origin. Oversized output is also downscaled to the page width you meant
(2102 px for 8.9 cm at 600 dpi).

---

## How the formatting works (and what it deliberately preserves)

The heavy lifting lives in the vendored scripts, unchanged:

- **Layout by structure** — layers are classified (`main` / `inset` / `linked_inset` /
  overlay / `linked_ext`), then either gridded, stacked, or page-scaled. A vertical
  stack of spectra (XPS peak fits, MS comparisons) counts as **one** panel.
- **Axis titles are measured, not guessed** — `fit_axis_titles` writes a title,
  renders, measures the ink with Pillow, derives the anchor offset for that graph,
  and corrects over several rounds. Fixed offsets do not work: two graphs with the
  same frame and font size rendered their titles 0.28 cm apart.
- **Overlaps are resolved vertically only** — `clamp_texts` pulls annotations back
  inside the frame, then `separate_texts` pushes them apart **vertically**. Horizontal
  nudging would move a label off the peak it points at.
- **Inset layers only ever shrink** — tick label sizes inside a millimetre-scale
  inset are left alone; enlarging them fuses `7.2` and `7.0` into `7.27.0`.
- **Never rewritten**: legend strings (their own `|`-separated syntax with per-entry
  escapes — wrapping the whole string bolds entry 1 and appends a stray `)`), `\g()`
  Symbol-font regions (stripping the inner `\f:` wrapper turns `C` into Chi), data
  values, axis ranges, curve colours, and inserted image content.

`README-origin-batch-style.md` is the original engineers' log — several hundred lines
of measured failure modes (LabTalk silently ignoring writes, linked-layer stale
frames, expgraph's undocumented argument traps). Read it before changing the scripts.

---

## Limitations

- **Windows + a licensed Origin installation** are hard requirements; without Origin
  the tools have nothing to drive.
- **One graph takes ~20–30 s.** Each call starts the Origin COM session and runs the
  render-measure-correct loop. That step cannot be made fast; the payoff is that
  everything *after* it (`origin_edit_*`, `thesis_export_open`) is sub-second.
- **Origin is an exclusive resource.** Formatting uses the single-instance COM server,
  so do not edit figures by hand in Origin while a batch is running.
- **Python ≤ 3.9 for the interpreter** until OriginLab ships newer wheels.
- `PER_GRAPH_TWEAKS` ships **empty**. Per-figure exceptions are inherently
  project-specific (which label collided with which curve, how many millimetres a text
  box had to move), so the table is documented but not populated. Add your own entries
  in the shape shown next to it in `format_thesis_figures.py` — the outer key is the
  project file stem, because `Graph8` means different things in different projects.
  It is applied automatically when a project matches, and `no_tweaks=true` skips it.

---

## Relationship to the upstream `origin-batch-style` toolkit

The three vendored scripts come from a private, actively used plotting toolkit of the
same author (`origin-batch-style/`, driven from the command line). This repository ships
a **curated variant** of them:

| | upstream (private) | this repository |
|---|---|---|
| Per-figure exception table (`PER_GRAPH_TWEAKS`) | populated for specific manuscripts | **empty**, scheme documented instead |
| Comments / docstrings | name the real samples, figures and project files | genericized (technical insights kept verbatim) |
| Usage examples | machine-specific interpreter paths | `python` |
| Everything else — layout logic, anchor calibration, overlap resolution, export and dpi correction | identical | identical |

**If you maintain your own copy, it will not automatically receive fixes made here**, and
vice versa. Two ways to keep them apart cleanly:

- Keep your private toolkit as the source of truth for *your* figures and point
  `DSH_THESIS_BATCH_DIR` at it. This repository's copy is then only the fallback for a
  fresh install — no sync problem, because you never use it.
- Or track this repository and re-apply your per-figure tweaks on top, using
  `sync_to_vendor.ps1` to push the result into the DSH runtime directory.

The regression used when this variant was cut: reformat a 13-graph project (0 errors,
0 verify issues) and re-open the saved product to verify it read-only (`clean: true`).

---

## Repository layout

```
thesis_mcp_server.py          MCP server (stdlib only; no `mcp` package needed)
start-thesis-mcp.cmd          launcher: self-locates via %~dp0, picks the interpreter
styles.json                   style profiles — the file you edit to change the look
cordis.patch.yml              DSH loader entry (names the launcher; no toolchain paths)
index.js                      bundle entry artifact (no-op apply)
format_thesis_figures.py  ┐
format_composite.py       ├── vendored plotting scripts (replaceable via
export_figures.py         ┘    DSH_THESIS_BATCH_DIR)
README-thesis.md              internal reference: every tool, parameter and pitfall
README-origin-batch-style.md  the original scripts' engineering log
sync_to_vendor.ps1            copy this repo to the DSH runtime directory
THIRD-PARTY-NOTICES.md        Origin / originpro / dependency licensing and runtime needs
CONTRIBUTING.md               how to run the smoke test, what needs discussion first
tests/smoke.py                installation self-check (14 assertions, no DSH needed)
```

## Licence

MIT — see [LICENSE](LICENSE). Origin itself is commercial software licensed separately;
`originpro` / `OriginExt` are BSD-licensed by OriginLab and installed by you, not
redistributed here. Full details, including the optional runtime dependencies of the
vendored scripts, are in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).