Compare commits

...

6 Commits

Author SHA1 Message Date
codex-bot 5d858ab8e2 Keep publisher namespaces separate in RSS identity regression
Metadata integrity checks / metadata (push) Successful in 2m6s
2026-11-09 18:03:16 +08:00
codex-bot bf4d0cd38a Collect RSS 2025 proceedings with explicit program evidence
Metadata integrity checks / metadata (push) Failing after 1m58s
2026-11-09 18:00:40 +08:00
Zayden-Jung ec9edd88d4 Merge pull request '保留ICLR历史录用档案的轨道与完整来源证据' (#41) from codex/iclr-archive into main
Metadata integrity checks / metadata (push) Successful in 2m54s
Reviewed-on: #41
2026-11-09 09:02:49 +00:00
Zayden-Jung 8ad052f577 Merge pull request '核验学习与规划会议前身卷并补齐发现范围' (#40) from codex/historical-predecessors into main
Metadata integrity checks / metadata (push) Successful in 2m12s
Reviewed-on: #40
2026-11-09 08:13:52 +00:00
codex-bot aa696bd7b5 Isolate tiny predecessor fixtures from production disk reserve
Metadata integrity checks / metadata (push) Successful in 2m11s
2026-11-09 15:15:48 +08:00
codex-bot 90bd534edd Preserve reviewed historical learning and planning predecessor series
Metadata integrity checks / metadata (push) Failing after 2m0s
2026-11-09 15:01:12 +08:00
20 changed files with 10545 additions and 4 deletions
+1
View File
@@ -51,6 +51,7 @@ jobs:
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','www-archive','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','aamas-proceedings','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','iclr-archive','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','rss-proceedings','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','postgres','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','edition-tracks','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','joint-editions','--help'],cwd=outside,check=True)
+4
View File
@@ -8,6 +8,8 @@ Paper Atlas 将 arXiv、AI 会议论文集及公开投稿决定整理到可追
历史 ICLR 2014–2016 官方录用页由 `collect iclr-archive` 适配,保留主会/workshop及重复展示证据;运行和身份核验边界见 [历史档案说明](docs/coverage/iclr-archive-2014-2016.md)。
RSS 2025 的 `collect rss-proceedings` 读取官方逐篇标题、完整作者、摘要和 DOI,并核对录用与演示节目单;不下载 PDF。15 个演示 ID 有明确来源,其余逐篇投稿类型保持未明确,不用标题或会话主题代替 track。运行、证据恢复和限制见 [RSS 元数据说明](docs/rss-proceedings.md)。
## 项目结构
```text
@@ -64,6 +66,8 @@ paper-atlas collect colm --cache /data/paper-index/raw/http-cache \
早期主系列的Workshop名称按[ICML/IROS精确届次证据](docs/coverage/historical-primary-series-2026-11-09.md)识别,保留原名与范围证据;代码支持不代表已补齐生产数据。
EWSL、EPS、ECP、EWSP 的[具体前身卷与完整快照要求](docs/coverage/historical-predecessors-2026-11-09.md)单独核验,保留原始系列及事件/出版年份;旧候选文件缺少 ECP/EWSP 时会明确提示,避免静默遗漏。
AAMAS 的[官方论文集采集器](docs/aamas-proceedings.md)目前核验了 2012 版式,可从缓存或已校验快照采集标题、作者及页面提供的摘要;发表形式单独保存,全文/扩展摘要中未明确的投稿 track 保持待明确,不根据会话主题推定主会轨道。
`paper-atlas collect --help` 查看来源;`paper-atlas import --help` 查看导入方式。完整批次步骤见[使用指南](docs/usage.md)。
@@ -0,0 +1,53 @@
# EWSL 与规划会议前身的来源范围
本项扩展 DBLP 元数据适配:核对具体历史卷,保留 EWSL、EPS、ECP、EWSP 原始系列名称,并使用已有会议别名关系关联 ECML-PKDD 或 ICAPS。它不把历史论文的原始会议名称替换成后来的会议,也不从卷的存在推断有逐篇数据。
## 证据与已核验边界
[ECML-PKDD 2011 官方历史页](https://ecmlpkdd2011.org/pastConferences.php)与 [ICAPS 官方系列历史](https://www.icaps-conference.org/conference-series/)分别提供系列前身关系。2026-11-09 冻结页面 SHA-256 为:
- ECML-PKDD:`c3ecdb032aeacf6a365aace5457773827d1405872ad9bc6be6fc05851d5e68d6`。
- ICAPS:`de1d217660cb819825f58112cdaf774d0f68da6ed87e3e90a59055e52f152081`。
与固定 DBLP 2026-11-01 快照的完整 proceedings 目录对照后,仅对下列具体卷识别前身主系列:
| 原始系列 | 事件年 | DBLP 卷 key | 卷出版年 | 原始逐篇书目计数 |
|---|---:|---|---:|---:|
| EWSL | 1987 | `conf/ecml/1987` | 1987 | 16 |
| EWSL | 1988 | `conf/ecml/1988` | 1988 | 21 |
| EWSL | 1989 | `conf/ecml/1989` | 1989 | 0 |
| EWSL | 1991 | `conf/ecml/1991` | 1991 | 44 |
| EPS | 1990 | `conf/aips/1990` | 1990 | 48 |
| ECP | 1997 | `conf/ecp/1997` | 1997 | 35 |
| ECP | 1999 | `conf/ecp/1999` | 2000 | 28 |
| EWSP | 1991 | `conf/ewsp/1991` | 1991 | 10 |
合计 202 条是原始 `inproceedings` 书目计数,尚未经过前言等材料排除、身份核查与生产验收,不能作为新论文数量。EWSL 1989 在当前 catalogue 中有卷信息,但未匹配到 `inproceedings` 条目;其他记录类型和其他来源仍待核查。代码支持它的范围规则不表示已补上这一年的论文。
核验同时要求来源 family、booktitle 基名、空轨道后缀、完整 proceedings key、完整标题及卷出版年与固定证据一致。邻近年份、标题附加内容、`w` 卷后缀或工作坊 booktitle 均不能继承此范围。ECP、EWSP 及 EPS 的未知卷保持待审。原有 AIPS、ICML/IROS、AISTATS/COLT 规则保持各自边界。
EWSL 的主轨道仍属于 `EWSL`,通过 `ECML-PKDD` 别名纳入家族查询;EPS、ECP、EWSP 保留原名,通过 `ICAPS` 别名关联。`main` 在此表示该原始系列的主轨道。原始 booktitle、作者、论文书目年、卷标题、卷出版年、事件年及历史页 URL 分开保存在 metadata 中。例如 ECP 1999 的事件年 1999 与卷出版年 2000 都保留,不用其中一个覆盖另一个。
## 候选文件与完整快照
旧的窄范围 `dblp-ai-candidates.jsonl` 未纳入 `ecp`、`ewsp` family,但完整 catalogue 的 proceedings 与 volume_counts 中有它们。新的 catalogue 导出会纳入这两个 family。
选择 ICAPS 且截止年达到 EWSP 1991 或 ECP 1997 时,若候选 manifest 缺少相应 family,`collect dblp --candidates` 会明确报错,要求使用原始完整 `--archive`。错误发生在写输出之前,不会把遗漏当作成功采集。选择其他会议或截止 EPS 1990 的旧候选采集不受这项新家族检查影响。
在已确认的远程运行环境中使用完整快照:
```sh
paper-atlas collect dblp \
--catalogue /data/paper-index/collected/dblp/dblp-catalogue.sqlite \
--archive /path/to/pinned-dblp.xml.gz --dtd /path/to/pinned-dblp.dtd \
--venue ECML-PKDD --venue ICAPS --through-year 2001 \
--out /data/paper-index/collected/historical-predecessors
```
完整快照与 DTD 均沿用现有固定身份校验。该命令也会输出已覆盖的其他年度;生产导入前须单独筛选目标卷与年度,检查 raw source、前言材料、稳定标识、标题、作者及现有投稿,不能直接导入整个输出目录。不会下载论文 PDF。
## 验证和当前限制
回归检查使用独立保存的八卷原始目录 fixture,合成论文只用于隔离测试;不代表真实逐篇来源。检查原始系列/别名、完整标题和年份边界、旧候选文件显式失败、扩展 family 的 catalogue 导出、原生导入后的元数据与关系。
代码、测试、采集和生产导入是不同阶段。新范围不表示本批数据已采集或已导入,更不表示公开拒稿、摘要或最终去重完整。原始书目仅提供发表条目证据,不是原始决定信;没有记录不能推断为拒稿或未举办。后续各批需保留独立验收报告,保护既有跨会投稿、决定与标签历史。
+38
View File
@@ -0,0 +1,38 @@
# RSS 官方论文元数据
这是长期维护、按需运行的 package collector,不是定时服务或一次性导入脚本。目前仅接受独立核对过的 2025 年版式;不把 2026 年或其他年份套用同一分类规则。
## 数据范围和证据
- [RSS XXI 官方论文集](https://www.roboticsproceedings.org/rss21/)列出 163 个不同的逐篇 HTML 链接。每篇有 citation 元数据、BibTeX 和摘要区;PDF 链接作为原始元数据保存,不请求全文。
- [官方 Accepted Papers](https://roboticsconference.org/2025/program/papers/)按论文 ID、标题和 session 对账。网页作者列包含隐藏的重复展示,逐篇有序 citation_author 与论文集目录作者完整对比后才采用;不截断作者、不将重复展示当新论文。
- [官方 Demos](https://roboticsconference.org/2025/program/demos/)明确列出 15 个论文 ID,包括标题不以 Demonstrating 开头的 ID 93。这些条目标为 `demo`,保留整行节目证据。其他 148 条使用 `proceedings_unspecified`:没有逐篇类型依据时,不因缺席演示表就自动推断 main。
- [作者说明](https://roboticsconference.org/2025/information/authorinfo/)描述 Science/Systems 与 Demo 两类投稿,两类都进入论文集。分类属于该来源的投稿/发表语义,不自动生成通用 research_type 标签。
`rss_proceedings:session` 保留官方 session,`rss_proceedings:program_role` 保留明确的 demonstration。二者与其他平台标签隔离;session 主题不等于投稿 track。单篇来源 ID 是 `2025/pNNN`,DOI 同时保留原始 BibTeX 大小写和标准化身份键,显式的 `publication_doi` 供原生导入器在同一会议年度/track 中关联其他正式发表来源;不能据此合并不同 track 或不同会议的投稿经历。
只有已发表、被官方录用表列出的条目才产生 accepted 记录。采集观察时间来自响应回执;出版日期单独留在 metadata,不伪造原始决定日期。来源未列出的论文不能推断为 rejected、desk rejected 或 withdrawn;公开负面决定是另一个待核范围。
## 运行
构建、安装和项目测试仅使用已确认的 `ces-control:host` 远程 runner。本机不能执行隐式构建的验证。服务器已安装环境运行示例:
```sh
paper-atlas collect rss-proceedings --year 2025 \
--snapshots /data/paper-index/collected/rss2025-full-evidence-v1 \
--out /data/paper-index/collected/rss2025-source-v1
```
也可使用 `--cache /data/paper-index/raw/rss-http` 请求限定官方 HTML 元数据。两种输入互斥。冻结快照的 `receipts.json` 每项必须包含 `file`、准确 `url/final_url`、`http_status: 200`、`content_type: text/html`、`bytes`、`sha256` 和含时区的观察时间 `at`。路径必须位于快照内,不能通过符号链接逃逸。所有输入逐个核验字节数与哈希,UTF-8 解码;重定向、未知版式和异常 ID 需要先核查。
采集只输出 JSONL、每个 track 的 manifest、完整 HTML 证据和 `rss-2025.json` 来源报告,不连接生产数据库。两份清单状态保持 `partial`;即使该卷 163 个链接全部解析,也不表示独立作品去重、逐篇类型或公开负面决定全集已完成。明确为空的摘要保存 `null` 和原因;缺失或重复摘要边界属于版式异常,停止输出。
## 冻结与恢复
2026-11-09 在服务器冻结 167 个官方 HTML 响应:目录、录用表、演示表、作者说明和 163 个逐篇页面。回执 SHA256:`f427d93a113702030864e789efe1176d69f71d2c567e81536d155e53d9513a93`。六个未改写样例位于 `tests/fixtures/rss2025/`,附其真实响应回执;完整原件保存在服务器来源目录与独立审阅备份,不作为运行时下载依赖。
整个采集持有输出目录的排他锁。全部解析和交叉检查通过后才发布 JSONL、manifest 和完成报告;完成报告最后发布。已有完成报告或 JSONL 时拒绝覆盖。若发布中断,先核对目录、回执和所有文件哈希,保留现场,再换新输出目录重采;不能看到日志安静就重启或重复导入。新的官方页面内容或计数与冻结样本不同,必须另留快照、解释变化并验证后再更新采集器。
生产导入前必须另外做来源独立核验和 PostgreSQL 只读身份预检。DOI、同标题候选和既有 arXiv 实体需要逐项核查,保留跨会的所有投稿和决定关系;发现冲突时隔离,不能只按标题强合并。已导入队列不得重放。生产独立验收通过后才重新统计年度缺口;导入成功和 CI 不能替代最终全库去重、摘要/标签及约束和实际查询审计。
本说明中的 163/15 是冻结官方页面的来源清单,不是当前生产库导入结果。历史缺口在本批生产验收前仍为 47。
+1
View File
@@ -14,6 +14,7 @@ COMMANDS = {
('collect','pmlr'): ('paper_atlas.collectors.pmlr', {}, 'PMLR 官方元数据'),
('collect','acl'): ('paper_atlas.collectors.acl', {}, 'ACL Anthology XML'),
('collect','iclr-archive'): ('paper_atlas.collectors.iclr_archive', {}, '核对ICLR 2014–2016官方历史录用档案'),
('collect','rss-proceedings'): ('paper_atlas.collectors.rss', {}, '核对RSS 2025官方论文摘要与演示证据'),
('collect','iclr-program'): ('paper_atlas.collectors.iclr', {}, 'ICLR 官方公开程序'),
('collect','iclr-snapshot'): ('paper_atlas.collectors.openreview', {}, 'ICLR 公开投稿与决定快照'),
('collect','public-openreview'): ('paper_atlas.collectors.public_openreview', {}, '核验其他会议的公开拒稿及撤稿标签'),
+1 -1
View File
@@ -20,7 +20,7 @@ DTD_MD5='96e4283e71886dec2173718957293648'
DTD_NAMES={'dblp.dtd','dblp-2023-06-28.dtd'}
RELEASE='https://doi.org/10.4230/dblp.xml.2026-11-01'
# Broad discovery candidates, including historical names; not a venue mapping.
FAMILIES=set('nips icml iclr colm colt aistats uai clear acl emnlp naacl eacl coling conll cvpr iccv eccv aaai ijcai ecai kdd sigir www wsdm recsys cikm icdm sdm ecml pkdd ecmlpkdd icra iros rss corl aips icaps kr atal aamas icassp interspeech eurospeech icslp ismir miccai fat facct lrec mm mlsys'.split())
FAMILIES=set('nips icml iclr colm colt aistats uai clear acl emnlp naacl eacl coling conll cvpr iccv eccv aaai ijcai ecai kdd sigir www wsdm recsys cikm icdm sdm ecml pkdd ecmlpkdd icra iros rss corl aips icaps ecp ewsp kr atal aamas icassp interspeech eurospeech icslp ismir miccai fat facct lrec mm mlsys'.split())
def records(archive,dtd_bytes):
@@ -0,0 +1,45 @@
"""Exact predecessor volumes, with original names and independent family histories."""
ECML_HISTORY = 'https://ecmlpkdd2011.org/pastConferences.php'
ICAPS_HISTORY = 'https://www.icaps-conference.org/conference-series/'
# family/year: booktitle, original series, target family, publication year, full title
VOLUMES = {
('ecml', 1987): ('EWSL', 'EWSL', 'ECML-PKDD', 1987,
'Progress in Machine Learning - Proceedings of EWSL 87: 2nd European Working Session on Learning, Bled, Yugoslavia, May 1987'),
('ecml', 1988): ('EWSL', 'EWSL', 'ECML-PKDD', 1988,
'Proceedings of the Third European Working Session on Learning, EWSL 1988, Turing Institute, Glasgow, UK, October 3-5, 1988'),
('ecml', 1989): ('EWSL', 'EWSL', 'ECML-PKDD', 1989,
'Proceedings of the 4th European Working Session on Learning, Montpellier, France, 4-6 December 1989'),
('ecml', 1991): ('EWSL', 'EWSL', 'ECML-PKDD', 1991,
'Machine Learning - EWSL-91, European Working Session on Learning, Porto, Portugal, March 6-8, 1991, Proceedings'),
('aips', 1990): ('Expert Planning Systems', 'EPS', 'ICAPS', 1990,
'Proceedings of the First International Conference on Expert Planning Systems, Brighton, UK, June 27-29, 1990'),
('ecp', 1997): ('ECP', 'ECP', 'ICAPS', 1997,
"Recent Advances in AI Planning, 4th European Conference on Planning, ECP'97, Toulouse, France, September 24-26, 1997, Proceedings"),
('ecp', 1999): ('ECP', 'ECP', 'ICAPS', 2000,
"Recent Advances in AI Planning, 5th European Conference on Planning, ECP'99, Durham, UK, September 8-10, 1999, Proceedings"),
('ewsp', 1991): ('EWSP', 'EWSP', 'ICAPS', 1991,
"European Workshop on Planning, EWSP '91, Sankt Augustin, Germany, March 18-19, 1991, Proceedings"),
}
def predecessor_scope(family, base, suffix, volume, year, title):
definition = VOLUMES.get((family, year))
if definition is None:
return None
book, original, target, publication_year, expected_title = definition
if (suffix or base != book or volume['key'] != f'conf/{family}/{year}'
or title != expected_title or volume['fields'].get('year') != [str(publication_year)]):
return None
return {'original_series': original, 'target_family': target,
'history_url': ECML_HISTORY if target == 'ECML-PKDD' else ICAPS_HISTORY,
'proceedings_key': volume['key'], 'proceedings_title': title,
'event_year': year, 'proceedings_year_fields': list(volume['fields']['year'])}
def missing_candidate_families(targets, families, through_year):
# These two families were absent from the earlier narrowed candidate JSONL,
# although their proceedings and volume_counts exist in the full catalogue.
required = {family for (family, year), definition in VOLUMES.items()
if family in ('ecp', 'ewsp') and definition[2] in targets and year <= through_year}
return sorted(family for family in required if not families.get(family))
+5 -1
View File
@@ -15,6 +15,7 @@ from urllib.parse import parse_qs,unquote,urlsplit
from paper_atlas.config import DEFAULT_REGISTRY
from paper_atlas.collectors.dblp import XML_SHA,DTD_MD5,RELEASE,records
from paper_atlas.collectors.dblp_scope import scope
from paper_atlas.collectors.dblp_predecessors import missing_candidate_families
from paper_atlas.db.submissions import timestamp
from paper_atlas.io.snapshots import digest_file
from paper_atlas.io.publication_ids import mlsys_id
@@ -88,6 +89,8 @@ def normalize_row(row,volumes,targets,include_workshops=False,through_year=2026)
'scope_rule':edition['rule'],'author_names_raw':fields.get('author',[]),'publtype':row.get('publtype'),
'bibliography':{key:value for key,value in fields.items() if key in ('booktitle','year','ee','url','pages','volume','number','publisher','series','isbn')},
'ambiguous_identifiers':ambiguous,'publication_evidence':'inproceedings bibliography; not an original decision note'}
if edition.get('historical_predecessor_evidence'):
metadata['historical_predecessor_evidence'] = edition['historical_predecessor_evidence']
if edition.get('joint_edition_evidence'):
metadata['joint_edition_evidence'] = edition['joint_edition_evidence']
if edition.get('track_scope_note'):
@@ -124,8 +127,9 @@ def collect(catalogue,out,venues=None,candidates=None,archive=None,dtd=None,incl
families = manifest.get('candidate_family_counts', {})
missing = [venue for venue, family in [('MM', 'mm'), ('MLSys', 'mlsys')]
if venue in targets and not families.get(family)]
missing += ['ICAPS/'+family for family in missing_candidate_families(targets, families, through_year)]
if missing:
raise ValueError('Candidate file predates requested families ' + ', '.join(missing) + '; use the original --archive')
raise ValueError('Candidate file omits requested families ' + ', '.join(missing) + '; use the original --archive')
out=Path(out);out.mkdir(parents=True,exist_ok=True)
with (out/'dblp.normalize.lock').open('a') as lock:
fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
+11 -2
View File
@@ -1,6 +1,8 @@
"""Explicit bibliographic aliases; a directory family alone is not venue evidence."""
import re
from paper_atlas.collectors.dblp_predecessors import predecessor_scope
# Ordered long/joint names precede their shorter forms.
BOOKS = {
@@ -27,7 +29,8 @@ BOOKS = {
'pkdd':[('ECML/PKDD','ECML-PKDD',[]),('ECML PKDD','ECML-PKDD',[]),('PKDD','PKDD',['ECML-PKDD'])],
'icra':[('ICRA','ICRA',[])], 'iros':[('IROS','IROS',[])],
'rss':[('Robotics: Science and Systems','RSS',[])], 'corl':[('CoRL','CoRL',[])],
'aips':[('ICAPS','ICAPS',[]),('AIPS','AIPS',['ICAPS'])], 'icaps':[('ICAPS','ICAPS',[])],
'aips':[('ICAPS','ICAPS',[]),('AIPS','AIPS',['ICAPS']),('Expert Planning Systems','EPS',['ICAPS'])], 'icaps':[('ICAPS','ICAPS',[])],
'ecp':[('ECP','ECP',['ICAPS'])], 'ewsp':[('EWSP','EWSP',['ICAPS'])],
'kr':[('KR','KR',[])], 'atal':[('AAMAS','AAMAS',[])],
'icassp':[('ICASSP','ICASSP',[])],
'interspeech':[('INTERSPEECH','INTERSPEECH',[]),('EUROSPEECH','EUROSPEECH',['INTERSPEECH']),('ICSLP','ICSLP',['INTERSPEECH']),('ECST','ECST',['INTERSPEECH'])],
@@ -135,7 +138,11 @@ def scope(row,volume,through_year=2026):
if len(bibliographic)!=1 or not re.fullmatch(r'\d{4}',bibliographic[0]):return None,'ambiguous_bibliographic_year'
if int(bibliographic[0])!=year and not re.search(r'\b'+str(year)+r'\b',title):return None,'unverified_year_difference'
text=(suffix+' '+title).casefold();track='main'
historical = historical_track(family, base, suffix, volume['key'], year, title)
predecessor = predecessor_scope(family, base, suffix, volume, year, title)
if (family in ('ecp', 'ewsp') or (family == 'aips' and base == 'Expert Planning Systems')) and not predecessor:
return None, 'unverified_historical_predecessor'
historical = (('main', 'Historical predecessor; exact volume, full title, event and publication years verified; original series retained.', predecessor['history_url'])
if predecessor else historical_track(family, base, suffix, volume['key'], year, title))
if historical:track=historical[0]
elif 'findings' in text:track='findings'
elif 'shared task' in text:track='shared_task'
@@ -165,6 +172,8 @@ def scope(row,volume,through_year=2026):
joint_evidence = 'https://www.eurai.org/ecai/history'
result = {'venue':venue,'venue_aliases':aliases,'year':year,'track':track,'bibliographic_year':int(bibliographic[0]),
'rule':family+':'+base,'proceedings_title':title,'edition_year_basis':year_basis}
if predecessor:
result['historical_predecessor_evidence'] = predecessor
if joint_evidence:
result['joint_edition_evidence'] = joint_evidence
if historical:
+279
View File
@@ -0,0 +1,279 @@
"""Reviewed RSS 2025 proceedings HTML; no PDF or private review access."""
import argparse
from collections import Counter
from datetime import date
import fcntl
import hashlib
import json
from pathlib import Path
import re
import tempfile
import unicodedata
from paper_atlas.db.submissions import timestamp
from paper_atlas.io.html import Document, Node, clean
from paper_atlas.io.http import fetch
SOURCE = 'rss_proceedings'
ROOT = 'https://www.roboticsproceedings.org/rss21/'
HEADING = 'Robotics: Science and Systems XXI'
PAGES = {'index': ROOT,
'accepted': 'https://roboticsconference.org/2025/program/papers/',
'demos': 'https://roboticsconference.org/2025/program/demos/',
'authorinfo': 'https://roboticsconference.org/2025/information/authorinfo/'}
EXPECTED = 163
DEMO_IDS = {2, 20, 21, 33, 58, 59, 62, 84, 86, 92, 93, 107, 129, 132, 141}
def one(values, description):
if len(values) != 1:
raise ValueError('Expected one '+description)
return values[0]
def comparable(text):
return unicodedata.normalize('NFC', clean(text))
def parse_index(html):
document = Document(html)
nodes = list(document.root.walk())
if [clean(n.text()) for n in nodes if n.tag == 'h1'] != [HEADING]:
raise ValueError('Unexpected RSS proceedings edition')
result = {}
for row in (n for n in nodes if n.tag == 'tr'):
links = [n for n in row.walk() if n.tag == 'a'
and re.fullmatch(r'p[0-9]{3}\.html', n.attrs.get('href', ''))]
if not links:
continue
link = one(links, 'proceedings entry link')
pid = int(link.attrs['href'][1:4])
title = clean(link.text())
authors = clean(one([n for n in row.walk() if n.tag == 'i'], 'index authors').text())
if pid in result or pid < 1 or not title or not authors:
raise ValueError('Empty or repeated RSS index entry')
result[pid] = {'title': title, 'authors_display': authors,
'url': ROOT+link.attrs['href']}
occurrences = sum(n.tag == 'a' and bool(re.fullmatch(r'p[0-9]{3}\.html', n.attrs.get('href', '')))
for n in nodes)
if not result or len(result) != occurrences:
raise ValueError('Unparsed or duplicate proceedings links')
return result
def parse_program(html, heading, columns):
nodes = list(Document(html).root.walk())
if [clean(n.text()) for n in nodes if n.tag == 'h1'] != [heading]:
raise ValueError('Unexpected RSS program page')
headers = [[clean(n.text()) for n in row.children if isinstance(n, Node) and n.tag == 'th']
for row in nodes if row.tag == 'tr']
if list(columns) not in headers:
raise ValueError('Unexpected RSS program columns')
result = {}
for row in (n for n in nodes if n.tag == 'tr'):
cells = [n for n in row.children if isinstance(n, Node) and n.tag == 'td']
if not cells:
continue
if len(cells) != len(columns):
raise ValueError('Unexpected RSS program row width')
raw_id = clean(cells[0].text())
if not re.fullmatch('[1-9][0-9]*', raw_id):
raise ValueError('Invalid program ID')
pid = int(raw_id)
if pid in result:
raise ValueError('Duplicate program ID')
result[pid] = {key: clean(node.text()) for key, node in zip(columns, cells)}
if not result[pid]['Title']:
raise ValueError('Missing program title')
if not result:
raise ValueError('No RSS program rows')
return result
def validate_program(index, accepted, demos, authorinfo):
if set(index) != set(accepted) or len(index) != EXPECTED or set(demos) != DEMO_IDS:
raise ValueError('RSS program and reviewed proceedings census differ')
text = clean(Document(authorinfo).root.text())
if ('There are two types of submissions to RSS' not in text
or 'Both types of papers appear in the RSS proceedings.' not in text):
raise ValueError('Submission category evidence changed')
for pid, item in index.items():
variants = [accepted[pid]['Title']] + ([demos[pid]['Title']] if pid in demos else [])
if any(comparable(title) != comparable(item['title']) for title in variants):
raise ValueError('Conflicting official program title for '+str(pid))
def parse_paper(html, pid, entry, accepted, demo, evidence):
nodes = list(Document(html).root.walk())
meta = {}
for node in nodes:
if node.tag == 'meta' and node.attrs.get('name', '').startswith('citation_'):
meta.setdefault(node.attrs['name'], []).append(node.attrs.get('content', ''))
title = one(meta.get('citation_title', []), 'citation title')
authors = meta.get('citation_author', [])
publication_date = one(meta.get('citation_publication_date', []), 'publication date')
if not re.fullmatch(r'2025/[0-9]{2}/[0-9]{2}', publication_date):
raise ValueError('Unexpected publication year/date')
date.fromisoformat(publication_date.replace('/', '-'))
if (one(meta.get('citation_conference_title', []), 'conference') != HEADING
or one(meta.get('citation_volume', []), 'volume') != '21'
or [clean(n.text()) for n in nodes if n.tag == 'h1'] != [HEADING]
or [comparable(n.text()) for n in nodes if n.tag == 'h3'] != [comparable(title)]
or comparable(title) != comparable(entry['title']) or not authors
or any(not clean(author) for author in authors)
or comparable(', '.join(authors)) != comparable(entry['authors_display'])):
raise ValueError('Proceedings/citation title, authors or edition disagree')
pdf = one(meta.get('citation_pdf_url', []), 'citation PDF URL')
if pdf not in (ROOT+f'p{pid:03d}.pdf', ROOT.replace('https:', 'http:')+f'p{pid:03d}.pdf'):
raise ValueError('Citation PDF ID differs; URL is metadata only')
bibtex = one([n.text() for n in nodes if n.tag == 'pre'], 'original BibTeX')
doi = one(re.findall(r'^\s*DOI\s*=\s*\{([^{}]+)\}', bibtex, re.I | re.M), 'BibTeX DOI')
year = one(re.findall(r'^\s*YEAR\s*=\s*\{([^{}]+)\}', bibtex, re.I | re.M), 'BibTeX year')
if doi != f'10.15607/RSS.2025.XXI.{pid:03d}' or year != '2025':
raise ValueError('BibTeX DOI/year differs from volume and paper ID')
abstract_parts = []
inside = False
markers = []
for node in (n for n in nodes if n.tag == 'p'):
labels = [clean(n.text()) for n in node.walk() if n.tag == 'b']
if 'Abstract:' in labels:
markers.append('start')
inside = True
value = clean(node.text()).removeprefix('Abstract:').strip()
if value:
abstract_parts.append(value)
elif 'Download:' in labels:
markers.append('end')
inside = False
elif inside:
abstract_parts.append(clean(node.text()))
if markers != ['start', 'end']:
raise ValueError('Abstract boundaries missing or repeated')
abstract = clean(' '.join(abstract_parts)) or None
track = 'demo' if demo else 'proceedings_unspecified'
tags = [{'namespace': SOURCE+':session', 'value': accepted['Session']}]
if demo:
tags.append({'namespace': SOURCE+':program_role', 'value': 'demonstration'})
return {'source': SOURCE, 'external_id': f'2025/p{pid:03d}', 'url': entry['url'],
'title': title, 'abstract': abstract, 'authors': authors,
'identifiers': {'doi': doi.lower()},
'submission_identifiers': [{'scheme': 'publication_doi', 'value': doi.lower()}],
'venue': 'RSS', 'venue_aliases': [], 'year': 2025, 'track': track,
'status': 'accepted', 'observed_at': evidence['paper']['observed_at'],
'raw_decision': 'listed in official RSS 2025 proceedings and accepted program',
'tags': tags,
'metadata': {'publisher_paper_id': pid, 'citation_metadata': meta,
'publication_date': publication_date, 'original_bibtex': bibtex,
'index_entry': entry, 'accepted_program_entry': accepted,
'demo_program_entry': demo, 'source_evidence': evidence,
'missing_abstract_reason': None if abstract else 'empty_official_abstract_section',
'track_scope_note': ('Explicit official demo program ID; not inferred from title.' if demo else
'Proceedings and accepted list do not individually identify submission type; session is not a track.'),
'decision_date_precision': 'not_supplied',
'publication_evidence': 'Published proceedings and accepted program; not an original decision note.'}}
class Pages:
def __init__(self, *, cache=None, snapshots=None):
if bool(cache) == bool(snapshots):
raise ValueError('Choose exactly one cache or frozen snapshot directory')
self.cache = Path(cache) if cache else None
self.root = Path(snapshots).resolve() if snapshots else None
self.receipts = json.loads((self.root/'receipts.json').read_bytes())['receipts'] if self.root else []
self.evidence = {}
self.data = {}
def read(self, url):
if self.root:
receipt = one([r for r in self.receipts if r['url'] == url], 'exact frozen response receipt')
filename = Path(receipt['file'])
path = self.root/filename
if (filename.is_absolute() or '..' in filename.parts or not path.resolve().is_relative_to(self.root)
or receipt['http_status'] != 200 or receipt['content_type'] != 'text/html'
or receipt.get('final_url', url) != url):
raise ValueError('Unsafe or unsuccessful frozen response')
data = path.read_bytes()
observed = receipt['at']
else:
data = fetch(url, self.cache, 2*1024**2, delay=1)
receipt = json.loads((self.cache/hashlib.sha256(url.encode()).hexdigest()).with_suffix('.json').read_bytes())
observed = receipt['retrieved_at']
if (len(data) > 2*1024**2 or len(data) != receipt['bytes']
or hashlib.sha256(data).hexdigest() != receipt['sha256'] or receipt['url'] != url):
raise ValueError('Response checksum, URL or size differs')
evidence = {'url': url, 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
'file': 'evidence/'+hashlib.sha256(data).hexdigest()+'.html',
'observed_at': timestamp(observed), 'http_status': 200, 'response_receipt': receipt}
self.evidence[url] = evidence
self.data[url] = data
return data.decode('utf-8'), evidence
def collect(year, out, *, cache=None, snapshots=None):
if year != 2025:
raise ValueError('Only the independently reviewed RSS 2025 edition is supported')
out = Path(out)
out.mkdir(parents=True, exist_ok=True)
with (out/'rss.collect.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
report_path = out/'rss-2025.json'
if report_path.exists() or list(out.glob('rss-2025-*.jsonl')):
raise FileExistsError('Inspect existing output; never overwrite a completed or partial queue')
pages = Pages(cache=cache, snapshots=snapshots)
context = {key: pages.read(url) for key, url in PAGES.items()}
index = parse_index(context['index'][0])
accepted = parse_program(context['accepted'][0], 'Accepted Papers', ('ID', 'Session', 'Title', 'Authors'))
demos = parse_program(context['demos'][0], 'Demos', ('ID', 'Location', 'Title', 'Day', 'Time'))
validate_program(index, accepted, demos, context['authorinfo'][0])
records = []
for pid, entry in sorted(index.items()):
html, evidence = pages.read(entry['url'])
sources = {key: item[1] for key, item in context.items()}
sources['paper'] = evidence
records.append(parse_paper(html, pid, entry, accepted[pid], demos.get(pid), sources))
counts = Counter(r['track'] for r in records)
if counts != {'demo': len(DEMO_IDS), 'proceedings_unspecified': EXPECTED-len(DEMO_IDS)}:
raise ValueError('Track census differs; review official source changes')
files = []
with tempfile.TemporaryDirectory(prefix='.rss-', dir=out) as temporary:
stage = Path(temporary)
(stage/'evidence').mkdir()
for url, data in pages.data.items():
(stage/pages.evidence[url]['file']).write_bytes(data)
for track in sorted(counts):
group = [r for r in records if r['track'] == track]
path = stage/f'rss-2025-{track}.jsonl'
path.write_text(''.join(json.dumps(r, ensure_ascii=False)+'\n' for r in group))
manifest = {'source': SOURCE, 'parser_version': 1, 'venue': 'RSS', 'year': 2025,
'track': track, 'count': len(group), 'file': str(out/path.name),
'file_sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
'list_url': ROOT, 'status': 'partial',
'missing_abstract_count': sum(r['abstract'] is None for r in group)}
path.with_suffix('.manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2)+'\n')
files.append(manifest)
result = {'source': SOURCE, 'year': 2025, 'records': len(records), 'files': files,
'source_evidence': list(pages.evidence.values()), 'database_written': False,
'pdfs_downloaded': False, 'status': 'collected_pending_identity_review',
'note': 'Only the published proceedings. Demo IDs have explicit program evidence; '
'other individual submission types remain unknown. No absent-based negative decisions. '
'Separate source identity review and independent production acceptance are required.'}
(stage/report_path.name).write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n')
(out/'evidence').mkdir(exist_ok=True)
for path in (stage/'evidence').iterdir():
path.replace(out/'evidence'/path.name)
for path in stage.iterdir():
if path.is_file() and path.name != report_path.name:
path.replace(out/path.name)
(stage/report_path.name).replace(report_path)
return result
def main(argv=None, *, prog=None):
parser = argparse.ArgumentParser(prog=prog)
parser.add_argument('--year', type=int, choices=[2025], required=True)
parser.add_argument('--out', required=True)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--cache')
group.add_argument('--snapshots')
args = parser.parse_args(argv)
print(json.dumps(collect(args.year, args.out, cache=args.cache, snapshots=args.snapshots), ensure_ascii=False))
+314
View File
@@ -0,0 +1,314 @@
{
"source_snapshot_sha256": "0376ad019b216553faff0a14e1757b2d4c7a3b84c1beacf0511ef83695211b18",
"scope": "Pinned proceedings metadata, not authentic example paper records or evidence that all volumes contain indexed papers. Test papers are synthetic.",
"volumes": [
{
"key": "conf/ecml/1987",
"fields": {
"editor": [
"Ivan Bratko",
"Nada Lavrac"
],
"title": [
"Progress in Machine Learning - Proceedings of EWSL 87: 2nd European Working Session on Learning, Bled, Yugoslavia, May 1987"
],
"booktitle": [
"EWSL"
],
"publisher": [
"Sigma Press, Wilmslow"
],
"year": [
"1987"
],
"isbn": [
"1-85058-088-X"
],
"url": [
"db/conf/ecml/ewsl87.html"
]
},
"bibliographic_counts": [
{
"year": "1987",
"booktitle": "EWSL",
"record_type": "inproceedings",
"count": 16
}
],
"expected_original_series": "EWSL",
"expected_target_family": "ECML-PKDD"
},
{
"key": "conf/ecml/1988",
"fields": {
"editor": [
"Derek H. Sleeman"
],
"title": [
"Proceedings of the Third European Working Session on Learning, EWSL 1988, Turing Institute, Glasgow, UK, October 3-5, 1988"
],
"booktitle": [
"EWSL"
],
"publisher": [
"Pitman Publishing"
],
"year": [
"1988"
],
"isbn": [
"0-273-08800-9"
],
"url": [
"db/conf/ecml/ewsl88.html"
]
},
"bibliographic_counts": [
{
"year": "1988",
"booktitle": "EWSL",
"record_type": "inproceedings",
"count": 21
}
],
"expected_original_series": "EWSL",
"expected_target_family": "ECML-PKDD"
},
{
"key": "conf/ecml/1989",
"fields": {
"editor": [
"Katharina Morik"
],
"title": [
"Proceedings of the 4th European Working Session on Learning, Montpellier, France, 4-6 December 1989"
],
"booktitle": [
"EWSL"
],
"publisher": [
"Pitman and Morgan Kaufman Publishers"
],
"year": [
"1989"
]
},
"bibliographic_counts": [],
"expected_original_series": "EWSL",
"expected_target_family": "ECML-PKDD"
},
{
"key": "conf/ecml/1991",
"fields": {
"editor": [
"Yves Kodratoff"
],
"title": [
"Machine Learning - EWSL-91, European Working Session on Learning, Porto, Portugal, March 6-8, 1991, Proceedings"
],
"booktitle": [
"EWSL"
],
"series": [
"Lecture Notes in Computer Science"
],
"volume": [
"482"
],
"publisher": [
"Springer"
],
"year": [
"1991"
],
"isbn": [
"3-540-53816-X"
],
"ee": [
"https://doi.org/10.1007/BFb0016999"
],
"url": [
"db/conf/ecml/ewsl91.html"
]
},
"bibliographic_counts": [
{
"year": "1991",
"booktitle": "EWSL",
"record_type": "inproceedings",
"count": 44
}
],
"expected_original_series": "EWSL",
"expected_target_family": "ECML-PKDD"
},
{
"key": "conf/ecp/1997",
"fields": {
"editor": [
"Sam Steel",
"Rachid Alami 0001"
],
"title": [
"Recent Advances in AI Planning, 4th European Conference on Planning, ECP'97, Toulouse, France, September 24-26, 1997, Proceedings"
],
"booktitle": [
"ECP"
],
"series": [
"Lecture Notes in Computer Science"
],
"volume": [
"1348"
],
"publisher": [
"Springer"
],
"year": [
"1997"
],
"isbn": [
"3-540-63912-8"
],
"ee": [
"https://doi.org/10.1007/3-540-63912-8"
],
"url": [
"db/conf/ecp/ecp1997.html"
]
},
"bibliographic_counts": [
{
"year": "1997",
"booktitle": "ECP",
"record_type": "inproceedings",
"count": 35
}
],
"expected_original_series": "ECP",
"expected_target_family": "ICAPS"
},
{
"key": "conf/ecp/1999",
"fields": {
"editor": [
"Susanne Biundo",
"Maria Fox 0001"
],
"title": [
"Recent Advances in AI Planning, 5th European Conference on Planning, ECP'99, Durham, UK, September 8-10, 1999, Proceedings"
],
"booktitle": [
"ECP"
],
"series": [
"Lecture Notes in Computer Science"
],
"volume": [
"1809"
],
"publisher": [
"Springer"
],
"year": [
"2000"
],
"isbn": [
"3-540-67866-2"
],
"ee": [
"https://doi.org/10.1007/10720246"
],
"url": [
"db/conf/ecp/ecp1999.html"
]
},
"bibliographic_counts": [
{
"year": "1999",
"booktitle": "ECP",
"record_type": "inproceedings",
"count": 28
}
],
"expected_original_series": "ECP",
"expected_target_family": "ICAPS"
},
{
"key": "conf/ewsp/1991",
"fields": {
"editor": [
"Joachim Hertzberg"
],
"title": [
"European Workshop on Planning, EWSP '91, Sankt Augustin, Germany, March 18-19, 1991, Proceedings"
],
"booktitle": [
"EWSP"
],
"series": [
"Lecture Notes in Computer Science"
],
"volume": [
"522"
],
"publisher": [
"Springer"
],
"year": [
"1991"
],
"isbn": [
"3-540-54364-3"
],
"ee": [
"https://doi.org/10.1007/BFb0052944"
],
"url": [
"db/conf/ewsp/ewsp1991.html"
]
},
"bibliographic_counts": [
{
"year": "1991",
"booktitle": "EWSP",
"record_type": "inproceedings",
"count": 10
}
],
"expected_original_series": "EWSP",
"expected_target_family": "ICAPS"
},
{
"key": "conf/aips/1990",
"fields": {
"title": [
"Proceedings of the First International Conference on Expert Planning Systems, Brighton, UK, June 27-29, 1990"
],
"booktitle": [
"Expert Planning Systems"
],
"publisher": [
"AAAI"
],
"year": [
"1990"
],
"url": [
"db/conf/aips/aips1990.html"
]
},
"bibliographic_counts": [
{
"year": "1990",
"booktitle": "Expert Planning Systems",
"record_type": "inproceedings",
"count": 48
}
],
"expected_original_series": "EPS",
"expected_target_family": "ICAPS"
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1646
View File
File diff suppressed because it is too large Load Diff
+1585
View File
File diff suppressed because it is too large Load Diff
+148
View File
@@ -0,0 +1,148 @@
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
<meta name="citation_title" content="Learned Perceptive Forward Dynamics Model for Safe and Platform-aware Robotic Navigation" />
<meta name="citation_author" content="Pascal Roth" />
<meta name="citation_author" content="Jonas Frey" />
<meta name="citation_author" content="Cesar Cadena" />
<meta name="citation_author" content="Marco Hutter" />
<meta name="citation_publication_date" content="2025/06/21" />
<meta name="citation_conference_title" content="Robotics: Science and Systems XXI" />
<meta name="citation_isbn" content="979-8-9902848-1-4" />
<meta name="citation_volume" content="21" />
<meta name="citation_pdf_url" content="http://www.roboticsproceedings.org/rss21/p001.pdf" />
<title>Robotics: Science and Systems XXI - Online Proceedings</title>
<link type="text/css" rel="stylesheet" href="../style.css">
<!--[if IE 5]>
<link type="text/css" rel="stylesheet" href="../ie5.css">
<![endif]-->
<!--[if IE 6]>
<link type="text/css" rel="stylesheet" href="../ie6.css">
<![endif]-->
</head>
<body>
<div class="menu">
<ul>
<li class="here"><a class="menu" href="../index.html">Home</a></li>
<li class="label">RSS XXII</span></li>
<li class="menu"><a class="menu" href="../rss22/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss22/authors.html">Authors</a></li>
<li class="label">RSS XXI</span></li>
<li class="menu"><a class="menu" href="../rss21/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss21/authors.html">Authors</a></li>
<li class="label">RSS XX</span></li>
<li class="menu"><a class="menu" href="../rss20/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss20/authors.html">Authors</a></li>
<li class="label">RSS XIX</span></li>
<li class="menu"><a class="menu" href="../rss19/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss19/authors.html">Authors</a></li>
<li class="label">RSS XVIII</span></li>
<li class="menu"><a class="menu" href="../rss18/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss18/authors.html">Authors</a></li>
<li class="label">RSS XVII</span></li>
<li class="menu"><a class="menu" href="../rss17/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss17/authors.html">Authors</a></li>
<li class="label">RSS XVI</span></li>
<li class="menu"><a class="menu" href="../rss16/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss16/authors.html">Authors</a></li>
<li class="label">RSS XV</span></li>
<li class="menu"><a class="menu" href="../rss15/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss15/authors.html">Authors</a></li>
<li class="label">RSS XIV</span></li>
<li class="menu"><a class="menu" href="../rss14/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss14/authors.html">Authors</a></li>
<li class="label">RSS XIII</span></li>
<li class="menu"><a class="menu" href="../rss13/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss13/authors.html">Authors</a></li>
<li class="label">RSS XII</span></li>
<li class="menu"><a class="menu" href="../rss12/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss12/authors.html">Authors</a></li>
<li class="label">RSS XI</span></li>
<li class="menu"><a class="menu" href="../rss11/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss11/authors.html">Authors</a></li>
<li class="label">RSS X</span></li>
<li class="menu"><a class="menu" href="../rss10/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss10/authors.html">Authors</a></li>
<li class="label">RSS IX</span></li>
<li class="menu"><a class="menu" href="../rss09/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss09/authors.html">Authors</a></li>
<li class="label">RSS VIII</span></li>
<li class="menu"><a class="menu" href="../rss08/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss08/authors.html">Authors</a></li>
<li class="label">RSS VII</span></li>
<li class="menu"><a class="menu" href="../rss07/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss07/authors.html">Authors</a></li>
<li class="label">RSS VI</span></li>
<li class="menu"><a class="menu" href="../rss06/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss06/authors.html">Authors</a></li>
<li class="label">RSS V</span></li>
<li class="menu"><a class="menu" href="../rss05/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss05/authors.html">Authors</a></li>
<li class="label">RSS IV</span></li>
<li class="menu"><a class="menu" href="../rss04/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss04/authors.html">Authors</a></li>
<li class="label">RSS III</span></li>
<li class="menu"><a class="menu" href="../rss03/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss03/authors.html">Authors</a></li>
<li class="label">RSS II</span></li>
<li class="menu"><a class="menu" href="../rss02/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss02/authors.html">Authors</a></li>
<li class="label">RSS I</span></li>
<li class="menu"><a class="menu" href="../rss01/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss01/authors.html">Authors</a></li>
<li class="label">Ethics and Malpractice</span></li>
<li class="menu"><a class="menu" href="../statement.html">Statement</a></li>
<li class="label">Contact</span></li>
<li class="last"><a class="menu" href="../contact.html">Webmaster</a></li>
<li class="no-style"><center><img border="0" src="../robot-small.jpg"></center>
<br><br><br>
<center>We are using google analytics for statistics!</center>
</ul>
</div>
<div class="content">
<h1>Robotics: Science and Systems XXI</h1>
<h3>Learned Perceptive Forward Dynamics Model for Safe and Platform-aware Robotic Navigation</h3>
<i>Pascal Roth, Jonas Frey, Cesar Cadena, Marco Hutter</i><br>
<p>
<b>Abstract:</b>
</p><p style="text-align: justify;">
Ensuring safe navigation in complex environments requires accurate real-time traversability assessment and understanding of environmental interactions relative to the robot's capabilities. Traditional methods, which assume simplified dynamics, often require designing and tuning cost functions to safely guide paths or actions toward the goal. This process is tedious, environment-dependent, and not generalizable. To overcome these issues, we propose a novel learned perceptive Forward Dynamics Model (FDM) that predicts the robot's future state conditioned on the surrounding geometry and history of proprioceptive measurements, proposing a more scalable, safer, and heuristic-free solution. The FDM is trained on multiple years of simulated navigation experience, including high-risk maneuvers, and real-world interactions to incorporate the full system dynamics beyond rigid body simulation. We integrate our perceptive FDM into a zero-shot Model Predictive Path Integral (MPPI) planning framework, leveraging the learned mapping between actions, future states, and failure probability. This allows for optimizing a simplified cost function, eliminating the need for extensive cost-tuning to ensure safety. On the legged robot ANYmal, the proposed perceptive FDM improves the position estimation by on average 41% over competitive baselines, which translates into a 27% higher navigation success rate in rough simulation environments. Moreover, we demonstrate effective sim-to-real transfer and showcase the benefit of training on synthetic and real data. Code and models are made publicly available under https://github.com/leggedrobotics/fdm.
</p>
<p>
<b>Download:</b> <a href="p001.pdf" target="_blank" onclick="_gaq.push(['_trackEvent','rss10/p001.pdf','PDF',this.href]);"><img src="../icon-pdf.png" border=0></a>
</p>
<p>
<b>Bibtex:</b>
<pre>
@INPROCEEDINGS{RothP-RSS-25,
AUTHOR = {Pascal Roth AND Jonas Frey AND Cesar Cadena AND Marco Hutter},
TITLE = {{Learned Perceptive Forward Dynamics Model for Safe and Platform-aware Robotic Navigation}},
BOOKTITLE = {Proceedings of Robotics: Science and Systems},
YEAR = {2025},
ADDRESS = {LosAngeles, CA, USA},
MONTH = {June},
DOI = {10.15607/RSS.2025.XXI.001}
}
</pre>
</p>
</div>
<div class="analyt">
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-24631810-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</div>
</body>
</html>
+155
View File
@@ -0,0 +1,155 @@
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
<meta name="citation_title" content="Demonstrating ViSafe: Vision-enabled Safety for High-speed Detect and Avoid" />
<meta name="citation_author" content="Parv Kapoor" />
<meta name="citation_author" content="Ian Higgins" />
<meta name="citation_author" content="Nikhil Varma Keetha" />
<meta name="citation_author" content="Jay Patrikar" />
<meta name="citation_author" content="Brady Moon" />
<meta name="citation_author" content="Zelin Ye" />
<meta name="citation_author" content="Yao He" />
<meta name="citation_author" content="Ivan Cisneros" />
<meta name="citation_author" content="Changliu Liu" />
<meta name="citation_author" content="Eunsuk Kang" />
<meta name="citation_author" content="Sebastian Scherer" />
<meta name="citation_publication_date" content="2025/06/21" />
<meta name="citation_conference_title" content="Robotics: Science and Systems XXI" />
<meta name="citation_isbn" content="979-8-9902848-1-4" />
<meta name="citation_volume" content="21" />
<meta name="citation_pdf_url" content="http://www.roboticsproceedings.org/rss21/p002.pdf" />
<title>Robotics: Science and Systems XXI - Online Proceedings</title>
<link type="text/css" rel="stylesheet" href="../style.css">
<!--[if IE 5]>
<link type="text/css" rel="stylesheet" href="../ie5.css">
<![endif]-->
<!--[if IE 6]>
<link type="text/css" rel="stylesheet" href="../ie6.css">
<![endif]-->
</head>
<body>
<div class="menu">
<ul>
<li class="here"><a class="menu" href="../index.html">Home</a></li>
<li class="label">RSS XXII</span></li>
<li class="menu"><a class="menu" href="../rss22/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss22/authors.html">Authors</a></li>
<li class="label">RSS XXI</span></li>
<li class="menu"><a class="menu" href="../rss21/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss21/authors.html">Authors</a></li>
<li class="label">RSS XX</span></li>
<li class="menu"><a class="menu" href="../rss20/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss20/authors.html">Authors</a></li>
<li class="label">RSS XIX</span></li>
<li class="menu"><a class="menu" href="../rss19/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss19/authors.html">Authors</a></li>
<li class="label">RSS XVIII</span></li>
<li class="menu"><a class="menu" href="../rss18/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss18/authors.html">Authors</a></li>
<li class="label">RSS XVII</span></li>
<li class="menu"><a class="menu" href="../rss17/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss17/authors.html">Authors</a></li>
<li class="label">RSS XVI</span></li>
<li class="menu"><a class="menu" href="../rss16/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss16/authors.html">Authors</a></li>
<li class="label">RSS XV</span></li>
<li class="menu"><a class="menu" href="../rss15/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss15/authors.html">Authors</a></li>
<li class="label">RSS XIV</span></li>
<li class="menu"><a class="menu" href="../rss14/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss14/authors.html">Authors</a></li>
<li class="label">RSS XIII</span></li>
<li class="menu"><a class="menu" href="../rss13/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss13/authors.html">Authors</a></li>
<li class="label">RSS XII</span></li>
<li class="menu"><a class="menu" href="../rss12/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss12/authors.html">Authors</a></li>
<li class="label">RSS XI</span></li>
<li class="menu"><a class="menu" href="../rss11/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss11/authors.html">Authors</a></li>
<li class="label">RSS X</span></li>
<li class="menu"><a class="menu" href="../rss10/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss10/authors.html">Authors</a></li>
<li class="label">RSS IX</span></li>
<li class="menu"><a class="menu" href="../rss09/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss09/authors.html">Authors</a></li>
<li class="label">RSS VIII</span></li>
<li class="menu"><a class="menu" href="../rss08/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss08/authors.html">Authors</a></li>
<li class="label">RSS VII</span></li>
<li class="menu"><a class="menu" href="../rss07/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss07/authors.html">Authors</a></li>
<li class="label">RSS VI</span></li>
<li class="menu"><a class="menu" href="../rss06/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss06/authors.html">Authors</a></li>
<li class="label">RSS V</span></li>
<li class="menu"><a class="menu" href="../rss05/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss05/authors.html">Authors</a></li>
<li class="label">RSS IV</span></li>
<li class="menu"><a class="menu" href="../rss04/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss04/authors.html">Authors</a></li>
<li class="label">RSS III</span></li>
<li class="menu"><a class="menu" href="../rss03/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss03/authors.html">Authors</a></li>
<li class="label">RSS II</span></li>
<li class="menu"><a class="menu" href="../rss02/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss02/authors.html">Authors</a></li>
<li class="label">RSS I</span></li>
<li class="menu"><a class="menu" href="../rss01/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss01/authors.html">Authors</a></li>
<li class="label">Ethics and Malpractice</span></li>
<li class="menu"><a class="menu" href="../statement.html">Statement</a></li>
<li class="label">Contact</span></li>
<li class="last"><a class="menu" href="../contact.html">Webmaster</a></li>
<li class="no-style"><center><img border="0" src="../robot-small.jpg"></center>
<br><br><br>
<center>We are using google analytics for statistics!</center>
</ul>
</div>
<div class="content">
<h1>Robotics: Science and Systems XXI</h1>
<h3>Demonstrating ViSafe: Vision-enabled Safety for High-speed Detect and Avoid</h3>
<i>Parv Kapoor, Ian Higgins, Nikhil Varma Keetha, Jay Patrikar, Brady Moon, Zelin Ye, Yao He, Ivan Cisneros, Changliu Liu, Eunsuk Kang, Sebastian Scherer</i><br>
<p>
<b>Abstract:</b>
</p><p style="text-align: justify;">
Assured safe-separation is essential for achieving seamless high-density operation of airborne vehicles in a shared airspace. To equip resource-constrained aerial systems with this safety-critical capability, we present ViSafe, a high-speed vision-only airborne collision avoidance system. ViSafe offers a full-stack solution to the Detect and Avoid (DAA) problem by tightly integrating a learning-based edge-AI framework with a custom multi-camera hardware prototype designed under SWaP-C constraints. By leveraging perceptual input-focused control barrier functions (CBF) to design, encode, and enforce safety thresholds, ViSafe can provide provably safe runtime guarantees for self-separation in high-speed aerial operations. We evaluate ViSafe’s performance through an extensive test campaign involving both simulated digital twins and real-world flight scenarios. By independently varying agent types, closure rates, interaction geometries, and environmental conditions (e.g., weather and lighting), we demonstrate that ViSafe consistently ensures self-separation across diverse scenarios. In first-of-its-kind real-world high-speed collision avoidance tests with closure rates reaching 144 km/h, ViSafe sets a new benchmark for vision-only autonomous collision avoidance, establishing a new standard for safety in high-speed aerial navigation.
</p>
<p>
<b>Download:</b> <a href="p002.pdf" target="_blank" onclick="_gaq.push(['_trackEvent','rss10/p002.pdf','PDF',this.href]);"><img src="../icon-pdf.png" border=0></a>
</p>
<p>
<b>Bibtex:</b>
<pre>
@INPROCEEDINGS{KapoorP-RSS-25,
AUTHOR = {Parv Kapoor AND Ian Higgins AND Nikhil Varma Keetha AND Jay Patrikar AND Brady Moon AND Zelin Ye AND Yao He AND Ivan Cisneros AND Changliu Liu AND Eunsuk Kang AND Sebastian Scherer},
TITLE = {{Demonstrating ViSafe: Vision-enabled Safety for High-speed Detect and Avoid}},
BOOKTITLE = {Proceedings of Robotics: Science and Systems},
YEAR = {2025},
ADDRESS = {LosAngeles, CA, USA},
MONTH = {June},
DOI = {10.15607/RSS.2025.XXI.002}
}
</pre>
</p>
</div>
<div class="analyt">
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-24631810-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</div>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
{
"scope": "Six unmodified official HTML fixtures; exact observations from full167 server freeze. No research PDFs.",
"receipts": [
{
"file": "index.html",
"url": "https://www.roboticsproceedings.org/rss21/",
"final_url": "https://www.roboticsproceedings.org/rss21/",
"http_status": 200,
"content_type": "text/html",
"bytes": 77753,
"sha256": "5d8e530b5ce7184795c14a7dca615f9c1a8b0b189661e0e6e795167b68b8516d",
"at": "2026-11-09T09:53:09.121733+00:00"
},
{
"file": "accepted.html",
"url": "https://roboticsconference.org/2025/program/papers/",
"final_url": "https://roboticsconference.org/2025/program/papers/",
"http_status": 200,
"content_type": "text/html",
"bytes": 197708,
"sha256": "fa1a5bce496a85eca8cec79aed8dbc148cda791fe6859375ba0c294983a18683",
"at": "2026-11-09T09:53:10.171450+00:00"
},
{
"file": "demos.html",
"url": "https://roboticsconference.org/2025/program/demos/",
"final_url": "https://roboticsconference.org/2025/program/demos/",
"http_status": 200,
"content_type": "text/html",
"bytes": 43542,
"sha256": "0b154ed110f6d49ebe2ba87f2c9c498df5fc1ea00bfe508b548522dfe4779698",
"at": "2026-11-09T09:53:11.145288+00:00"
},
{
"file": "authorinfo.html",
"url": "https://roboticsconference.org/2025/information/authorinfo/",
"final_url": "https://roboticsconference.org/2025/information/authorinfo/",
"http_status": 200,
"content_type": "text/html",
"bytes": 54508,
"sha256": "24d023daf9a074434c6e004001b863dfd260330e523671d614fa120b052927ab",
"at": "2026-11-09T09:53:12.170246+00:00"
},
{
"file": "p001.html",
"url": "https://www.roboticsproceedings.org/rss21/p001.html",
"final_url": "https://www.roboticsproceedings.org/rss21/p001.html",
"http_status": 200,
"content_type": "text/html",
"bytes": 9048,
"sha256": "973172348096a5cbf3e311bdc1ce33b27447761fa1058a050e611aa17547d821",
"at": "2026-11-09T09:53:13.437943+00:00"
},
{
"file": "p002.html",
"url": "https://www.roboticsproceedings.org/rss21/p002.html",
"final_url": "https://www.roboticsproceedings.org/rss21/p002.html",
"http_status": 200,
"content_type": "text/html",
"bytes": 9335,
"sha256": "30b5b68e7ed6a809ca51282c5efd1416513417ba91eba2845f2e73ed18b90ba4",
"at": "2026-11-09T09:53:14.697857+00:00"
}
]
}
+151
View File
@@ -0,0 +1,151 @@
"""Original series, exact edition boundaries, and complete candidate discovery."""
from contextlib import closing
from copy import deepcopy
import gzip
import hashlib
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from paper_atlas.collectors.dblp import catalogue, XML_SHA
from paper_atlas.collectors.dblp_records import collect, normalize_row
from paper_atlas.ingestion.records import ingest_file
FIXTURE = json.loads((Path(__file__).parent/'fixtures/historical_predecessors.json').read_text())
EXAMPLES = {row['key']: row for row in FIXTURE['volumes']}
def example(key, *, bibliographic_year=None):
fixed = EXAMPLES[key]
family = key.split('/')[1]
year = int(key.split('/')[-1])
volume = {'record_type': 'proceedings', 'key': key, 'fields': deepcopy(fixed['fields'])}
row = {'record_type': 'inproceedings', 'key': f'conf/{family}/SyntheticExample{year}', 'publtype': None,
'fields': {'title': [f'Synthetic historical research example {family} {year}'],
'author': ['Jane Doe 0001', 'John Smith'], 'booktitle': fixed['fields']['booktitle'],
'year': [str(bibliographic_year or year)], 'crossref': [key],
'ee': [f'https://doi.org/10.1234/synthetic.{family}.{year}']}}
return row, volume
class HistoricalPredecessorTests(unittest.TestCase):
def normalize(self, key, **kwargs):
row, volume = example(key, **kwargs)
fixed = EXAMPLES[key]
result, reason = normalize_row(row, {key: volume}, {fixed['expected_target_family']})
self.assertIsNone(reason)
self.assertIsNotNone(result)
return result
def test_ewsl_primary_series_retains_original_name_and_family_alias(self):
for year in [1987, 1988, 1989, 1991]:
with self.subTest(year=year):
record = self.normalize(f'conf/ecml/{year}')
self.assertEqual((record['venue'],record['venue_aliases'],record['year'],record['track']), ('EWSL',['ECML-PKDD'],year,'main'))
proof = record['metadata']['historical_predecessor_evidence']
self.assertEqual(proof['original_series'], 'EWSL')
self.assertEqual(proof['history_url'], 'https://ecmlpkdd2011.org/pastConferences.php')
self.assertEqual(record['metadata']['bibliography']['booktitle'], ['EWSL'])
self.assertEqual(record['metadata']['author_names_raw'], ['Jane Doe 0001','John Smith'])
self.assertEqual(record['authors'], ['Jane Doe','John Smith'])
self.assertEqual(EXAMPLES['conf/ecml/1989']['bibliographic_counts'], [])
def test_eps_ecp_ewsp_keep_original_series_instead_of_renaming_to_icaps(self):
for key in ['conf/aips/1990','conf/ecp/1997','conf/ecp/1999','conf/ewsp/1991']:
with self.subTest(key=key):
record = self.normalize(key)
fixed = EXAMPLES[key]
self.assertEqual(record['venue'], fixed['expected_original_series'])
self.assertEqual(record['venue_aliases'], ['ICAPS'])
self.assertEqual(record['track'], 'main')
proof = record['metadata']['historical_predecessor_evidence']
self.assertEqual(proof['history_url'], 'https://www.icaps-conference.org/conference-series/')
self.assertEqual(proof['proceedings_title'], fixed['fields']['title'][0])
self.assertEqual(proof['proceedings_year_fields'], fixed['fields']['year'])
def test_altered_titles_keys_suffixes_publication_years_and_unreviewed_editions_stay_held(self):
for key in ['conf/ecml/1988','conf/aips/1990','conf/ecp/1997','conf/ewsp/1991']:
row, volume = example(key)
for changed in ['title','key','suffix','publication_year']:
with self.subTest(key=key, changed=changed):
r, v = deepcopy(row), deepcopy(volume)
if changed == 'title': v['fields']['title'][0] += ', Affiliated Workshop'
elif changed == 'key': v['key'] += 'w'; r['fields']['crossref'] = [v['key']]
elif changed == 'suffix': r['fields']['booktitle'][0] += ' (Workshop)'
else: v['fields']['year'] = ['2001']
result, _ = normalize_row(r, {v['key']: v}, {EXAMPLES[key]['expected_target_family']})
self.assertIsNone(result)
row, volume = example('conf/ecp/1997')
row['fields']['year'] = ['2001']; row['fields']['crossref'] = ['conf/ecp/2001']
volume['key'] = 'conf/ecp/2001'; volume['fields']['year'] = ['2001']
self.assertIsNone(normalize_row(row, {volume['key']:volume}, {'ICAPS'})[0])
def test_ecp1999_event_and_bibliographic_years_are_independently_retained(self):
record = self.normalize('conf/ecp/1999', bibliographic_year=2000)
self.assertEqual(record['year'], 1999)
self.assertEqual(record['metadata']['bibliographic_year'], 2000)
self.assertEqual(record['metadata']['bibliography']['year'], ['2000'])
self.assertEqual(record['metadata']['historical_predecessor_evidence']['proceedings_year_fields'], ['2000'])
self.assertEqual(record['metadata']['historical_predecessor_evidence']['event_year'], 1999)
def test_old_candidates_require_new_families_only_when_requested_years_need_them(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary); row, volume = example('conf/aips/1990')
db = root/'dblp-catalogue.sqlite'
with closing(sqlite3.connect(db)) as connection, connection:
connection.execute('CREATE TABLE proceedings(key TEXT PRIMARY KEY,metadata_json TEXT)')
connection.execute('INSERT INTO proceedings VALUES(?,?)', (volume['key'],json.dumps(volume)))
candidate = root/'candidate.jsonl'; candidate.write_text(json.dumps(row)+'\n')
db.with_name('dblp-ai-candidates.manifest.json').write_text(json.dumps({
'snapshot_sha256': XML_SHA, 'candidate_sha256': hashlib.sha256(candidate.read_bytes()).hexdigest(),
'candidate_records': 1, 'candidate_family_counts': {'aips':1},
'record_types': {'proceedings':1,'inproceedings':1}}))
# Tiny CI fixtures may live on tmpfs; production reserve is tested separately.
with patch('paper_atlas.collectors.dblp_records.shutil.disk_usage', return_value=SimpleNamespace(free=16*1024**3)):
result = collect(db, root/'early', ['ICAPS'], candidates=candidate, through_year=1990)
self.assertEqual((result['normalized_records'],result['files'][0]['venue']), (1,'EPS'))
for year, missing in [(1991,'ICAPS/ewsp'),(1997,'ICAPS/ecp')]:
out = root/str(year); out.mkdir(); sentinel=out/'previous.txt';sentinel.write_text('accepted previous output')
with self.subTest(year=year), self.assertRaisesRegex(ValueError, missing):
collect(db, out, ['ICAPS'], candidates=candidate, through_year=year)
self.assertEqual(sentinel.read_text(), 'accepted previous output')
self.assertFalse((out/'dblp-normalization.json').exists())
def test_new_catalogue_includes_previously_missing_families_without_decision_inference(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary);dtd=root/'dblp.dtd';dtd.write_bytes(b'<!ENTITY eacute "&#233;">')
archive=root/'dblp.xml.gz'
body=''.join(f'<inproceedings key="conf/{family}/Synthetic"><title>Synthetic example</title><year>{year}</year><booktitle>{book}</booktitle><crossref>conf/{family}/{year}</crossref></inproceedings>' for family,year,book in [('ecp',1997,'ECP'),('ewsp',1991,'EWSP')])
with gzip.open(archive,'wb') as stream:stream.write(('<?xml version="1.0"?><!DOCTYPE dblp SYSTEM "dblp.dtd"><dblp>'+body+'</dblp>').encode())
result=catalogue(archive,dtd,root/'out',hashlib.sha256(archive.read_bytes()).hexdigest(),hashlib.md5(dtd.read_bytes()).hexdigest())
rows=[json.loads(line)for line in Path(result['file']).read_text().splitlines()]
self.assertEqual({r['key'] for r in rows}, {'conf/ecp/Synthetic','conf/ewsp/Synthetic'})
self.assertEqual(result['candidate_records'], 2)
self.assertTrue(all('status' not in r and 'venue' not in r for r in rows))
def test_native_import_keeps_original_series_aliases_and_full_source_evidence(self):
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary);db=root/'sample.sqlite'
for key in ['conf/ecml/1988','conf/ecp/1999']:
record=self.normalize(key);path=root/(key.replace('/','-')+'.jsonl')
path.write_text(json.dumps(record,ensure_ascii=False)+'\n')
path.with_suffix('.manifest.json').write_text(json.dumps({'source':'dblp','venue':record['venue'],'year':record['year'],'track':'main','count':1,'list_url':'https://doi.org/10.4230/dblp.xml.2026-11-01','file_sha256':hashlib.sha256(path.read_bytes()).hexdigest(),'status':'partial','missing_abstract_count':1}))
ingest_file(db,path)
with closing(sqlite3.connect(db)) as connection:
rows=connection.execute('SELECT v.acronym,a.alias,e.year FROM editions e JOIN venues v ON v.id=e.venue_id JOIN venue_aliases a ON a.venue_id=v.id ORDER BY v.acronym').fetchall()
self.assertEqual(rows,[('ECP','ICAPS',1999),('EWSL','ECML-PKDD',1988)])
metadata=[json.loads(r[0])for r in connection.execute('SELECT metadata_json FROM source_records')]
self.assertEqual({r['historical_predecessor_evidence']['original_series'] for r in metadata}, {'ECP','EWSL'})
self.assertEqual(connection.execute('SELECT count(*) FROM decisions WHERE status=?',('accepted',)).fetchone()[0],2)
self.assertEqual(connection.execute('PRAGMA foreign_key_check').fetchall(),[])
def test_existing_aips_normalization_remains_original_aips(self):
row, volume=example('conf/aips/1990');row['fields']['booktitle']=['AIPS'];row['fields']['year']=['1994'];row['fields']['crossref']=['conf/aips/1994']
volume['key']='conf/aips/1994';volume['fields']['title']=['AIPS 1994 Proceedings'];volume['fields']['year']=['1994']
record,reason=normalize_row(row,{volume['key']:volume},{'ICAPS'})
self.assertIsNone(reason);self.assertEqual((record['venue'],record['venue_aliases']),('AIPS',['ICAPS']))
self.assertNotIn('historical_predecessor_evidence',record['metadata'])
+186
View File
@@ -0,0 +1,186 @@
"""Frozen publisher examples, conflicting evidence and preserved attempt identity."""
import hashlib
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
from unittest.mock import patch
from paper_atlas.collectors import rss
from paper_atlas.ingestion.records import ingest_file
FIXTURES = Path(__file__).parent/'fixtures/rss2025'
OBSERVED = '2026-11-09T10:00:00Z'
def fixture(name):
return (FIXTURES/name).read_text()
class RssProceedingsTests(unittest.TestCase):
def setUp(self):
self.index = rss.parse_index(fixture('index.html'))
self.accepted = rss.parse_program(fixture('accepted.html'), 'Accepted Papers', ('ID', 'Session', 'Title', 'Authors'))
self.demos = rss.parse_program(fixture('demos.html'), 'Demos', ('ID', 'Location', 'Title', 'Day', 'Time'))
def paper(self, pid=1, html=None, entry=None):
return rss.parse_paper(html or fixture(f'p{pid:03d}.html'), pid, entry or self.index[pid],
self.accepted[pid], self.demos.get(pid), {'paper': {'observed_at': OBSERVED}})
def test_frozen_official_census_and_all_fixture_hashes(self):
receipts = json.loads((FIXTURES/'receipts.json').read_bytes())['receipts']
self.assertEqual(len(receipts), 6)
for r in receipts:
data = (FIXTURES/r['file']).read_bytes()
self.assertEqual((len(data), hashlib.sha256(data).hexdigest()), (r['bytes'], r['sha256']))
self.assertEqual(len(self.index), 163)
self.assertEqual(set(self.index), set(self.accepted))
self.assertEqual(len(self.demos), 15)
rss.validate_program(self.index, self.accepted, self.demos, fixture('authorinfo.html'))
self.assertFalse(self.demos[93]['Title'].startswith('Demonstrating'))
def test_complete_authors_abstract_and_source_metadata_are_preserved(self):
row = self.paper()
self.assertEqual(row['authors'], ['Pascal Roth', 'Jonas Frey', 'Cesar Cadena', 'Marco Hutter'])
self.assertTrue(row['abstract'].startswith('Ensuring safe navigation'))
self.assertTrue(row['abstract'].endswith('https://github.com/leggedrobotics/fdm.'))
self.assertNotIn('Download:', row['abstract'])
self.assertEqual(row['identifiers'], {'doi': '10.15607/rss.2025.xxi.001'})
self.assertEqual(row['metadata']['publication_date'], '2025/06/21')
self.assertIn('AUTHOR', row['metadata']['original_bibtex'])
self.assertEqual(row['status'], 'accepted')
self.assertNotIn('decision_date', row)
self.assertEqual(row['metadata']['decision_date_precision'], 'not_supplied')
def test_demo_evidence_is_independent_of_title_and_session_topic(self):
row = self.paper(2)
self.assertEqual(row['track'], 'demo')
self.assertEqual(row['metadata']['demo_program_entry']['ID'], '2')
self.assertEqual(self.paper()['track'], 'proceedings_unspecified')
self.assertEqual({t['namespace'] for t in row['tags']}, {'rss_proceedings:session', 'rss_proceedings:program_role'})
self.assertFalse(any('research_type' in t['namespace'] for t in row['tags']))
html = fixture('p001.html').replace('Learned Perceptive Forward Dynamics Model', 'Demonstrating a Learned Model')
entry = {**self.index[1], 'title': self.index[1]['title'].replace('Learned Perceptive Forward Dynamics Model', 'Demonstrating a Learned Model')}
self.assertEqual(self.paper(html=html, entry=entry)['track'], 'proceedings_unspecified')
def test_missing_abstract_is_explicit_and_multiple_paragraphs_are_complete(self):
html = fixture('p001.html')
beginning = html.index('<p style="text-align: justify;">')
end = html.index('</p>', beginning)
empty = html[:beginning]+'<p style="text-align: justify;">'+html[end:]
row = self.paper(html=empty)
self.assertIsNone(row['abstract'])
self.assertEqual(row['metadata']['missing_abstract_reason'], 'empty_official_abstract_section')
multi = html.replace('</p> \n<p> \n<b>Download:', '</p><p>Second abstract paragraph.</p><p><b>Download:', 1)
self.assertTrue(self.paper(html=multi)['abstract'].endswith('Second abstract paragraph.'))
def test_metadata_drift_author_truncation_and_wrong_paper_doi_fail(self):
original = fixture('p001.html')
for altered in [original.replace('XXI.001}', 'XXI.002}'),
original.replace('content="2025/06/21"', 'content="2026/06/21"'),
original.replace('<meta name="citation_author" content="Marco Hutter" />', ''),
original.replace('<b>Abstract:</b>', '<b>Summary:</b>'),
original.replace('rss21/p001.pdf', 'rss21/p002.pdf'),
original.replace('citation_volume" content="21', 'citation_volume" content="22')]:
with self.subTest(altered=altered[:60]), self.assertRaises(ValueError):
self.paper(html=altered)
def test_duplicate_and_unparsed_index_entries_fail(self):
for html in [fixture('index.html').replace('p002.html', 'p001.html'),
fixture('index.html')+'<a href="p999.html">Stray paper</a>',
fixture('index.html').replace( rss.HEADING, 'A different volume')]:
with self.subTest(html=html[:40]), self.assertRaises(ValueError):
rss.parse_index(html)
def test_program_title_id_category_and_header_changes_require_review(self):
for change in ['title', 'id', 'category']:
index = {k: dict(v) for k, v in self.index.items()}
demos = {k: dict(v) for k, v in self.demos.items()}
if change == 'title':
demos[2]['Title'] = 'Another paper'
elif change == 'id':
del index[1]
else:
del demos[93]
with self.subTest(change=change), self.assertRaises(ValueError):
rss.validate_program(index, self.accepted, demos, fixture('authorinfo.html'))
with self.assertRaises(ValueError):
rss.parse_program(fixture('demos.html').replace('>Location<', '>Other<'), 'Demos', ('ID', 'Location', 'Title', 'Day', 'Time'))
def snapshot(self, root):
root.mkdir()
data = (FIXTURES/'p001.html').read_bytes()
(root/'paper.html').write_bytes(data)
receipt = {'file': 'paper.html', 'url': self.index[1]['url'], 'final_url': self.index[1]['url'],
'http_status': 200, 'content_type': 'text/html', 'bytes': len(data),
'sha256': hashlib.sha256(data).hexdigest(), 'at': OBSERVED}
(root/'receipts.json').write_text(json.dumps({'receipts': [receipt]}))
return receipt
def test_frozen_receipt_hash_status_redirect_and_path_guards(self):
for failure in ['hash', 'status', 'redirect', 'path', 'duplicate', 'symlink']:
with self.subTest(failure=failure), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)/'source'
r = self.snapshot(root)
receipts = [r]
if failure == 'hash': r['sha256'] = '0'*64
elif failure == 'status': r['http_status'] = 404
elif failure == 'redirect': r['final_url'] = 'https://example.org/p001.html'
elif failure == 'path': r['file'] = '../outside.html'
elif failure == 'duplicate': receipts.append(dict(r))
else:
(root.parent/'outside.html').write_bytes((root/'paper.html').read_bytes())
(root/'paper.html').unlink()
(root/'paper.html').symlink_to(root.parent/'outside.html')
(root/'receipts.json').write_text(json.dumps({'receipts': receipts}))
with self.assertRaises(ValueError): rss.Pages(snapshots=root).read(self.index[1]['url'])
def test_unreviewed_edition_and_existing_output_refuse_work(self):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(ValueError): rss.collect(2026, tmp, snapshots=FIXTURES)
(Path(tmp)/'rss-2025.json').write_text('{}')
with self.assertRaises(FileExistsError): rss.collect(2025, tmp, snapshots=FIXTURES)
def test_native_roundtrip_is_idempotent_with_same_doi_and_distinct_tracks(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
original = self.paper()
dblp = {**original, 'source': 'dblp', 'external_id': 'conf/rss/example', 'metadata': {}, 'tags': []}
variant = {**original, 'external_id': '2025/other-track', 'track': 'workshop'}
for n, record in enumerate([dblp, original, variant, original]):
path = root/f'{n}.jsonl'
path.write_text(json.dumps(record)+'\n')
path.with_suffix('.manifest.json').write_text(json.dumps({'source': record['source'], 'list_url': rss.ROOT, 'count': 1}))
ingest_file(root/'sample.sqlite', path)
with sqlite3.connect(root/'sample.sqlite') as c:
self.assertEqual(c.execute('SELECT count(*) FROM papers').fetchone()[0], 1)
self.assertEqual(c.execute('SELECT count(*) FROM source_records').fetchone()[0], 3)
self.assertEqual(c.execute('SELECT count(*) FROM submissions').fetchone()[0], 2)
self.assertEqual(c.execute('SELECT count(*) FROM decisions').fetchone()[0], 2)
self.assertEqual(c.execute('SELECT count(*) FROM submission_sources').fetchone()[0], 3)
self.assertEqual(c.execute('PRAGMA foreign_key_check').fetchall(), [])
def test_two_real_papers_collection_commits_evidence_only_after_success(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
index = {pid: self.index[pid] for pid in [1, 2]}
accepted = {pid: self.accepted[pid] for pid in [1, 2]}
demos = {2: self.demos[2]}
with (patch.object(rss, 'EXPECTED', 2), patch.object(rss, 'DEMO_IDS', {2}),
patch.object(rss, 'parse_index', return_value=index),
patch.object(rss, 'parse_program', side_effect=[accepted, demos])):
result = rss.collect(2025, root/'out', snapshots=FIXTURES)
self.assertEqual((result['records'], len(result['files'])), (2, 2))
self.assertFalse(result['database_written'])
self.assertFalse(result['pdfs_downloaded'])
self.assertEqual(len(result['source_evidence']), 6)
for item in result['source_evidence']:
self.assertEqual(hashlib.sha256((root/'out'/item['file']).read_bytes()).hexdigest(), item['sha256'])
for item in result['files']:
self.assertEqual(item['missing_abstract_count'], 0)
self.assertEqual(hashlib.sha256(Path(item['file']).read_bytes()).hexdigest(), item['file_sha256'])
if __name__ == '__main__':
unittest.main()