Repository navigation
Dev 3.3 - #129
Draft
aozalevsky wants to merge 29 commits into
Draft
Dev 3.3#129aozalevsky wants to merge 29 commits into
aozalevsky wants to merge 29 commits into
Conversation
These figures are sized by their total width, so the axis labels, the categorical group labels and the legend are subtracted from the data area rather than added around it. Bokeh 3 reserves more room for that chrome than bokeh 2 did, so the plots came out of 70910fb noticeably tighter. The crosslinking-MS data quality plot suffered most, because its category labels are the longest in the report and it was also the only plot still at width=700 while the rest were at 800: its data area went from 192x68 to 119x61 px. MolProbity was down to 347x56, and the rest lost 4-9% of their width. Replace the assorted hardcoded 700/800 widths with a single constant, and pin the bar thickness with frame_height so it no longer depends on how much vertical space the title and axes happen to take. Data areas are now: crosslinking-MS data quality 119x61 -> 259x63 MolProbity 347x56 -> 487x63 excluded volume 467x17 -> 607x21 3DEM resolution 546x21 -> 586x21 crosslink satisfaction 405x21 -> 445x21 840 px keeps the widest of them (the model quality plots, which sit in a column layout that adds 30 px) inside the 540 pt printable width of the letter-sized PDF. Verified on 9a8w and 9a3w: no page overflows, page counts unchanged at 15 and 27. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
The HTML report embeds each plot as a bokeh JSON document and hands it to BokehJS from the CDN. layout.html pinned BokehJS 2.4.3, but 70910fb bumped the python package to 3.x, and a 3.x document is not readable by 2.x BokehJS - so every interactive plot in the HTML report has been coming out blank since that commit. Confirmed in a headless browser against 8zzc: with 2.4.3 all three plot containers on main.html are empty (0 children, 0 height); taking the version from the installed package instead gives 3.6.3 and the plots render again. Nothing caught this because the PDF report renders the SVG exports of the same figures, and those were unaffected. Take the version from bokeh.__version__ rather than pinning it in the template, so the two cannot drift apart again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
Every plot in the HTML report carried the same default toolset and no way
to read a value off it. Add a HoverTool to all of them, via a small
utility.add_hover() helper that ties the tool to explicit renderers - most
of these plots have helper glyphs (error bars, fit lines, threshold
markers) that would otherwise steal the hover with nothing to show.
Coverage, counted on 8zzc/9a8w/9a3w: 66 of 66 plots.
SAS (sas_plots.py) q/I, Kratky, Porod, P(r), Guinier and model fits,
plus the residual panels. Already had a
ColumnDataSource per plot, so this is just the tool.
quality at a glance metric name and the same formatted value shown in
the legend. The three plots built from literal
lists now build a source instead, so the tooltip
has a metric name to show.
3DEM (em.py) voxel values, volume estimate, RAPS, FSC and atom
inclusion. These pass bare lists to p.line(), so
they get a source and mode='vline' - the pointer
is rarely near enough a vertex otherwise.
crosslinking (cx.py) iqplot builds its own figures, so the per-model
points are picked out of p.renderers. The
distogram staircase can only report its whole
outline, so bins are overlaid with transparent
quads to give per-bin tooltips.
Verified in a headless browser, not just in the serialised output: the
tooltips appear and read e.g. "Model 1 / Satisfaction rate: 100.00 %" and
"q [nm-1]: 1.3578 / Log I(q): 1.506". All 69 tool instances resolve every
field they reference to a real column of the renderer's data source.
The PDF is unaffected: 65 of 72 SVG exports are byte identical, and the
rest only differ by an unreferenced clipPath or by the toolbar row getting
one icon taller. Data areas are identical everywhere and page counts are
unchanged. The transparent hover quads would otherwise leave an empty
<path> each in the exported SVG, so strip_bokeh_invisible_outline grew a
second pattern and a name to match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
These legends carry one entry per bar, so any mismatch between the legend
row and the bar accumulates down the plot. With ten SAS Rg datasets the
legend ran 26 px per row against 21 px bars and finished 67 px past the
bottom of the plot frame, each label further from the bar it names than
the last. Three- and six-bar plots looked fine only because there were not
enough rows for the drift to show.
Bokeh takes the legend row height as the tallest of the glyph, the label
box and the *measured* label text, then adds `spacing`. Nothing about the
glyph or the spacing can pull a row below the text itself, and at 14pt
that text measures 23 px - so the label font is the only lever. 12pt is
the largest size whose text fits inside a 21 px row for every label the
report produces ('2.98 nm', '88.38 %', '2.02'), and it puts the rows
exactly on the bars: on 8zzc the ten-row Rg legend and the six-row
chi-squared legend now span 189 px and 105 px, matching their bars.
The measurement is string-dependent, which is worth knowing before
touching this again: '2.02' comes out at 21 px even at 14pt, so a plot
whose labels are all plain numbers looks correct while one with units does
not.
Set it in one helper and drop the per-site font settings it supersedes -
five of those ran after add_layout and silently overrode it, which is an
easy trap to fall into twice.
Narrower legends also give the data areas 11-19 px more width. Verified on
8zzc, 9a8w and 9a3w: page counts unchanged at 51, 15 and 27.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
Each figure in the report already sets a transparent background so the PDF watermark shows through, but that does not cover the legend: a bokeh legend defaults to background_fill_color='#ffffff' at 0.95 alpha and punches a white rectangle back through the page. On 8zzc that was 25 opaque panels - 21 across the SAS plots, 3 on the 3DEM plots and the rest on the at-a-glance summaries - each clipping the watermark around itself. The helper takes the plot rather than the legend, so it also covers the SAS and 3DEM plots, which never build a Legend explicitly but get one from `legend_label` on their glyphs. Bokeh's splattable `plot.legend` accessor makes it a no-op on a plot with no legend, and applies to all of them where there is more than one. For the 3DEM plots it has to run after the glyphs rather than next to the other styling, because the legend does not exist until the first legend_label glyph is added. Verified on 8zzc, 9a8w and 9a3w: no opaque legend panels left in any of the 72 exported SVGs, page counts unchanged at 51, 15 and 27. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
This was the only at-a-glance plot with no legend, so its three numbers could not be read off the report at all - and they are the ones that need reading, because the bars share one axis and the sidechain count dominates it. On 9a3w the clashscore and Ramachandran bars take 7% and 9% of the width against a full-width sidechain bar. Give it the same legend the other plots have, at the bar pitch and with a transparent background, ordered to sit alongside the bars it labels - bokeh draws the first factor at the bottom, so the items have to run the other way. The outlier counts are also shown as a share of the residues they were measured over, which needed the totals: get_mq_plot_data was passing only the three numbers. It has a single consumer, so it now carries the denominators alongside them. A clashscore is a rate per 1000 atoms with nothing to divide by and is shown as it comes. 9a3w now reads 45.92, 58 (0.87%) and 651 (10.52%). The legend costs the data area width, 487 -> 313 px, which is the same trade every other plot in this set already makes. The axis label still says "Outliers" for all three, including the clashscore, and the shared scale still squashes the smaller two. Both are left alone deliberately. Scoped to the MolProbity branch: 9a8w and 8zzc are coarse-grained, never enter it, and rebuild byte-identical across all 63 of their SVGs. 9a3w stays at 27 pages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
Two kinds of duplication, neither of which changes a byte of what the report renders. static/ was a partial, stale mirror of the assets the website serves. Comparing git blob shas against rcsb/PDB-Dev-website website-files-production/, all seven files under static/js/ and css/bootstrap.carousel.css are byte-identical copies of files that repo owns, and nothing in this repo references any of them - every template takes its javascript from a CDN. css/old.css is not in that repo either and is referenced by nothing, so it was simply orphaned. That is 316 KB carried in every --html-mode local archive for nothing. The three files layout.html does use - bootstrap.min.css, main.css, layout.css - are byte-identical to the website's, so local-mode archives still match what a reader sees online. They stay. layout.html and full_validation_pdf.html each carried their own copy of the jquery/popper/bootstrap CDN tags. That is how the BokehJS tags ended up two major versions behind the plots they had to render, so the shared copy now lives in web_resources.j2 and says so. Deliberately left alone: report pages still load /css/bootstrap.min.css (4.0.0-beta.2) alongside the CDN's 4.5.2. Dropping either changes how the pages render against a website that is frozen, so the version-mismatch half of #34 stays open. Verified on 9a3w that this is invisible in the output: the emitted resource URLs are identical in content and order, all five HTML pages differ only in whitespace from the macro expansion, and the PDF is 27 pages with page 20 pixel-identical - the only pixels that move on page 1 are the generation timestamp. A --html-mode local build ships just the three CSS files, resolves every reference, and renders in a browser with BokehJS 3.6.3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
…122 --nocache reaches every validation module, and molprobity, precision and excludedvolume all gate their cache read on it. SAS never looked at it: get_sascif_file returned the cached .sascif whenever one existed, so --nocache silently did nothing for SAS. The same omission is in three more places, two of them character for character the same code: the EMDB pickle in em.py, the PRIDE pickle in cx.py, and the EMDB validation-analysis directory whose log line already calls itself a cache. Fixing only SAS would have left the flag broken for 3DEM and crosslinking, so all four now use the same `and not self.nocache` / `or self.nocache` shape as the modules that had it right. The validation-analysis run needed one thing more than a condition: run_va finishes by copying its whole output directory into place, which raises if one is already there, so a stale directory is removed before re-running. Verified by seeding a cache with a sentinel and calling get_sascif_file against both the old and new code: before, nocache=True kept the stale file; after, it refetches. Same check for the EMDB and PRIDE pickles with the network helpers stubbed. End to end on 8zzc, --nocache reports no cache hits and refreshes all six entries while still producing a 51 page report, and a normal run reports 13 cache hits and touches nothing. On 9a3w, --nocache logs "Running va from scratch" where it used to log "Found VA ... in cache". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
f1fn, f2fn and f3fn are built once per SASBDB entry and never read. No code anywhere writes or reads a _fit1.dat, _fit2.dat or _pval.dat, and no such file turns up in a populated cache, so these are leftovers from an approach that was replaced by reading the fits straight out of the sascif. Verified on 8zzc: all 52 exported SVGs byte-identical and the report is still 51 pages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
Whether a run is reading from the cache, and which directory it is using, was only inferable from the "Found ... in cache" lines that appear later - and when --nocache is in effect there are none of those at all, so there was nothing to distinguish it from a cold cache. Say it once up front. The path is resolved because --cache-root is usually given as a relative path, and the message for --nocache avoids calling the cache switched off: results are still written back for the next run, only reads are skipped. Related to #122. Cache is enabled: /path/to/cache Cache is disabled: results in /path/to/cache will be ignored and overwritten molprobity.py has its own --cache/--nocache in a standalone __main__ but never configures logging, so it gets nothing here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
A cache entry said nothing about where it came from. That matters most for
the expensive ones: a MolProbity result or a VA run is only meaningful
next to the versions that produced it, and there was no way to tell a
result computed last week from one computed two releases ago.
Every entry now carries when it was written, which IHMValidation wrote it,
and the versions of any external tool whose output is baked into it -
MolProbity for .mp.pkl, PrISM and PyMOL for .prism.pkl, and EMDB VA,
ChimeraX and MapQ for the VA directory. The excluded volume, EMDB and
PRIDE entries get no software list: one is our own arithmetic, the other
two are somebody else's data fetched verbatim.
Found 9a8w in cache: cache/9a8w.prism.pkl
(IHMValidation 3.2, 2026-08-19T18:36:14+00:00, PrISM dbe5a41, PyMOL 3.1.0)
Three things worth knowing about the shape of this:
Entries written before today are kept, not invalidated. unwrap_cache spots
an unwrapped pickle and hands it back with no metadata, and the log says
so. Throwing them away would have meant re-downloading hundreds of
megabytes of maps and re-running VA on every existing cache, which is a
steep toll for provenance.
VA is a directory, so it cannot be wrapped the way a pickle can and gets a
JSON sidecar instead. The name does not collide with any of the four globs
that read that directory. Collecting its tool versions is guarded per
tool, because failing to read a version is never a reason to lose a VA run
that takes minutes.
REPORT_VERSION moves to utility.IHMV_VERSION, with report.py re-exporting
it under the old name. This is forced rather than chosen: report.py
imports the assessment modules, so they cannot import it back to stamp
their caches.
Verified by reading the pickles directly as well as the log: legacy
entries load, fresh ones round-trip, and report output is untouched -
11/11 SVGs identical on 9a8w at 15 pages, and 9a3w still 27.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
The new VA output already carries the data behind the RCSB Q-score slider, it was just never read. For each model it gives the fraction of EMDB entries scoring at or below it, once against the whole archive and once against entries of comparable resolution, so higher is better on both. Both numbers are worth showing because they can disagree sharply: 9a3w is at the 32nd percentile of the archive but the 5th among entries of its own resolution, which is the more honest comparison and the one a reader would otherwise never see. The direction was checked against the va source rather than assumed - qscore_bar -> get_bar -> bar -> get_score, where the count is of entries scoring <= this model, divided by the total. Reading it backwards would have made a poor model look good in a validation report. em.py keeps the entry counts, resolution ranges and EMDB statistics version alongside the two values, because a percentile means nothing without knowing what it was measured against; they go in the hover. One plot per map, as MolProbity does per model. Folding every map into one grouped plot looks tidier but is not: bokeh puts a gap between factor groups and takes it out of the frame rather than adding to it, so three maps turned 21 px bars into 14 px ones with the labels overlapping, and the legend - which has no such gaps - drifted a bar-width further out of step with its bars for every map after the first. Maps are listed in the same order as the Q-score plot above, and only the first plot carries the heading. VA runs predating the slider have no bar data, so the plot is simply absent: verified by stripping qscore_bar from a real VA cache, which rebuilds cleanly with the plain Q-score plot intact. Coarse-grained entries never run VA and are unaffected. 9a0j 42 pages, 9a3w 27, 9a8w 15, and every hover field resolves. MolProbity percentiles, the other half of #112, still need a reference distribution from somewhere and are not addressed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
The PXD identifier was set to '14px' where everything around it is 14pt - about 10.5pt, so visibly smaller - and was left at bokeh's default grey rather than the #444444 the major labels use. It read as a faded annotation instead of part of the axis. Take colour, style and size from the major labels, the way the MolProbity and Q-score percentile plots already do, so all three grouped plots are styled identically rather than three different ways. The copy has to come after the major label sizing, which it does here. Costs a little width - the larger label leaves the data area at 257 px rather than 278 - but that is still well above where it started (192 in bokeh 2, 119 before the width fix earlier in this branch). 9a8w stays at 15 pages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
Bokeh labels every plot with font-family="helvetica". That is an alias, not an installed font: the browser that measures the text to lay an axis out and the wkhtmltopdf that finally renders it resolve it independently, and they do not agree. Text is drawn narrower than the space reserved for it, so right-aligned labels come out ragged, by more the longer the label is. In the crosslinking data quality plot "Matched" finished 18 px short of the axis while "Mapped to matching entities" finished 44 px short, which reads as a gap after the longest label. Arial is what helvetica resolves to here anyway, so naming it changes no typeface - it only makes the two sides agree. Slack drops to 3.6-6.7 px and stops depending on label length. The space bokeh had reserved for text wider than it draws comes back to the plots, which is worth more than the alignment: the crosslinking data quality frame goes 257 -> 321 px, MolProbity 313 -> 366, and the SAS Rg frame 504 -> 533. 8zzc comes out two pages shorter for the same content - 52 SVGs, 49 JSONs and all six sections still there. utility.set_plot_font walks the model tree rather than making every call site list its text properties, so it also reaches legends, categorical group labels and the figures iqplot builds for us. It is applied in each module's save_plots, plus the two per-model SVG exports in get_plots that happen before save_plots is reached. Verified by grep: 1439 font-family attributes across the SVGs of four entries, all Arial, no helvetica left. Page counts hold at 9a8w 15, 9a3w 27, 9a0j 42, with 8zzc 51 -> 49, and no page overflows its printable width. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
The branch has been on 3.3 while REPORT_VERSION still read 3.2, so the reports and, since the cache carries provenance, every freshly written cache entry claimed 3.2. One line: everything derives from utility.IHMV_VERSION - report.py re-exports it as REPORT_VERSION, the software table and the static pages render it, and the cache stamps it. Existing cache entries keep the version that actually produced them rather than being invalidated. That is the point of recording it: entries written before this commit still read 3.2, older ones have no metadata at all, and the log says which is which on every hit. Verified: the report now reads "IHMValidation Version 3.3" and a freshly written cache entry stamps ihmv_version 3.3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjiLLueZYqxEsPjEaSao2n
get_restraints() built four parallel lists but only appended to 'Restraint info' from inside a partial if/elif tree. GeometricRestraint, EPRRestraint, HDXRestraint and HydroxylRadicalFPRestraint had no branch, so those columns drifted apart: 8ZZC has 101 restraints and produced 41 descriptions. Every restraint now goes through one format_restraint_info() call site, so the columns cannot drift. The four missing classes have branches, and the bound flavours - harmonic, lower, upper, lower/upper - are formatted in one place shared by predicted-contact, derived-distance and geometric restraints, so a PredictedContactRestraint no longer reports a bare distance with its bound type dropped. Two branches that could never be reached are gone: UpperBoundDistanceRestraint derives from DistanceRestraint rather than Restraint, so it never matches anything in system.restraints, and no class of python-ihm has 'Mutagenesis' in its name. Counting crosslinks was wrong as well. experimental_cross_links is a list of lists, one sublist per ambiguous identification, and len() counted the sublists: 8ZZ4 has 8 crosslinks in 4 groups and was reported as 4. get_dataset_xl_info() looked its dataset up with .index(), which returns the first match. One crosslinking dataset can be built with several linkers, each of which python-ihm turns into a restraint of its own, so 8ZZ4 reported EGS and silently dropped BS3, BS2G and DST. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UcLBegC2XKi6difkEdevre
get_restraints_info() paired 'Restraint type' with 'Restraint info' by position, and when the two disagreed it ran set() over each column independently before pairing them again. That destroyed the row correspondence rather than repairing it: 8ZZC reported "1 unique GeometricRestraint: DSS, 508 crosslinks", and because set() iteration depends on the hash seed, three runs of the same entry gave three different wrong answers. The fallback is gone. The function now takes the restraints themselves instead of the table built by get_restraints(), which is what lets it add quantities up rather than list them, and reports one line per restraint type carrying its totals: Crosslinking-MS (615 groups, 1071 restraints) Geometric (60 restraints) SAS (36 restraints) The group and restraint counts are the ones section 5.2 already prints, from the same two numbers. What a restraint was derived from - the crosslinker, the resolution of a class average - describes the data rather than the restraint and belongs with the datasets, so it is left out here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UcLBegC2XKi6difkEdevre
What characterises experimental data - the crosslinker it was made with, the resolution of a class average, the radius of gyration of a scattering profile - is recorded on the restraints derived from a dataset rather than on the dataset itself, and so went unreported. Section 4.6 now carries it, after the values the report derives: SASDBX9: Rg from Gunier is 2.78 nm and Rg from p(r) is 2.64 nm ... PXD053341: 268 crosslinks (268 SDA) SASDBX9: deposited Rg is 2.25 nm Lines are keyed on the accession where a dataset has one, so a deposited value sits in the same row as whatever was derived for it. That makes the two comparable: SASDBX9 of 8ZZC deposits an Rg 19% below both derived values, and separately has the worst fit in the entry. The deposited Rg is converted from Angstroms to nanometres to match the units the rest of the row uses. This fills two gaps. Only 5 of 8ZZC's 36 SAS datasets are in SASBDB, so the other 31 had no data quality line at all, and 2DEM had none either. A dataset can be a mixture of crosslinkers, which python-ihm splits into a restraint per linker; those are added back up and broken down by linker rather than reported separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UcLBegC2XKi6difkEdevre
get_rg_for_plot() reads Rg_from_Guinier before Rg_from_PR, and get_rg_data() states them in that order in the summary table, but the at-a-glance plot mapped the first value to p(r). Every entry with SAS data has been reporting the two the wrong way round: the plot gave SASDBV9 of 8ZZC a Guinier Rg of 1.82 nm where section 4.6 of the same report gives 1.77 nm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UcLBegC2XKi6difkEdevre
Bokeh lays a categorical range out from the axis origin, which on a y axis is the bottom, so an entry with three model groups opened on the third one. top_down() reverses the factors, and each plot loses its legend.items.reverse(): that call existed only to undo the bottom-up range, and with the range flipped the legend lines up as built. All six enumerated plots are flipped, not only the crosslinking one. Leaving the rest would have had 9A0J list the same three maps in opposite directions in adjacent sections, with Q-score opening on EMD-10293 while 3DEM resolution opened on EMD-10291. The Q-score percentile plots stacked in reverse for the same reason, and now follow the order they are given in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UcLBegC2XKi6difkEdevre
EMDB has started serving map analyses from VA 0.0.1.dev151, which moves
the whole analysis one level down, under the EMDB ID, next to its own
version, validation_json, feature_assessment, audit_validation and
map_map_cc entries. get_emdb_map_validation() unwrapped the response
once, so every per-map key looked missing: 9A3W lost its slices,
voxel-value distribution, volume estimate, power spectrum and FSC, and
its full report shrank from 27 pages to 22. On 2026-10-07, 17 of the 47
maps behind the v3.3 release set are served this way; all 47 were flat
when that set was built in August.
The nested object is the dev137 analysis, unchanged. The outer version,
validation_json and feature_assessment describe the dev151 wrapper, and
its feature_assessment is empty, so unwrap_emdb_analysis() lifts the
inner analysis back to the top level, lets it win over the outer copies,
and keeps the two entries that only the wrapper has. Flat responses come
back as before. EMDB's own validation_json rejects the nested responses
too ("$.41983: 'orthogonal_projection' is a required property"), so the
layout may yet be reverted; both are read.
This does not repair EMD-*.pkl entries cached from a nested response:
their map plots were built from the broken analysis. Delete them and let
them be fetched again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3ycJ168m3kN9rd4kGVHTw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Main changes:
This is the first version that was actively developed using AI coding agents