Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d858ab8e2 | |||
| bf4d0cd38a |
@@ -50,8 +50,8 @@ jobs:
|
||||
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','cvf','--help'],cwd=outside,check=True)
|
||||
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','aamas-proceedings','--year','2025','--snapshots',str(src/'tests/fixtures/aamas2025'),'--out',str(root/'installed-aamas2025')],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)
|
||||
|
||||
@@ -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
|
||||
@@ -66,7 +68,7 @@ paper-atlas collect colm --cache /data/paper-index/raw/http-cache \
|
||||
|
||||
EWSL、EPS、ECP、EWSP 的[具体前身卷与完整快照要求](docs/coverage/historical-predecessors-2026-11-09.md)单独核验,保留原始系列及事件/出版年份;旧候选文件缺少 ECP/EWSP 时会明确提示,避免静默遗漏。
|
||||
|
||||
AAMAS 的[官方论文集采集器](docs/aamas-proceedings.md)支持已核验的 2012、2025 版式,可从缓存或已校验快照采集元数据。2025 年目录含 476 条非 keynote 出版物,保留完整作者和机构,HTML 未提供摘要时留空;发表形式与投稿 track 分开,不根据主题推定主会轨道或研究类型。
|
||||
AAMAS 的[官方论文集采集器](docs/aamas-proceedings.md)目前核验了 2012 版式,可从缓存或已校验快照采集标题、作者及页面提供的摘要;发表形式单独保存,全文/扩展摘要中未明确的投稿 track 保持待明确,不根据会话主题推定主会轨道。
|
||||
|
||||
`paper-atlas collect --help` 查看来源;`paper-atlas import --help` 查看导入方式。完整批次步骤见[使用指南](docs/usage.md)。
|
||||
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
# AAMAS 官方论文集元数据
|
||||
|
||||
`paper_atlas.collectors.aamas` 是按需运行的采集适配器,通过统一入口 `collect aamas-proceedings` 使用。支持已独立核验的 **AAMAS 2012、2025** HTML 版式;其他年份需先核对官方目录和逐篇结构,再增加适配,不能仅替换年份。2025 解析单独封装在 `paper_atlas.collectors.aamas2025`。
|
||||
|
||||
2025 版式使用[官方首页](https://www.ifaamas.org/Proceedings/aamas2025/)和[出版目录](https://www.ifaamas.org/Proceedings/aamas2025/forms/contents.htm)。原始目录共 479 条:3 keynote、260 research paper、148 extended abstract、12 Blue Sky、25 doctoral、22 Demo 栏目、9 JAAMAS。排除 keynote,输出 476 条;Demo 栏目中的 p3000 是明确标题的 COMP24 竞赛报告,发表年份仍为 2025,格式记 `competition_report`、投稿 track 留作 `proceedings_unspecified`,保留原 Demo 栏目证据。其余输出按 track 分组为 409 unspecified、12 blue_sky、25 doctoral、21 demo、9 jaamas。
|
||||
|
||||
作者按逐篇 BR 行解析,斜体机构原文另存,保留 Unicode、顺序及完整名单。HTML 没有提供摘要或逐篇 DOI,故 `abstract=null`、标识字典为空;只保存 PDF 链接,不下载研究 PDF。不能把 Research Paper Track 的所有条目推成 main,不能把目录缺席推为 rejected/withdrawn,也不能把出版格式标签当研究类型。
|
||||
|
||||
2025 用法:`paper-atlas collect aamas-proceedings --year 2025 --snapshots /data/review/aamas2025 --out /data/paper-index/collected/aamas-2025`;在线模式改用 `--cache /data/paper-index/cache/aamas2025`,只请求上述两个 HTML 页面。快照目录包含原始 `index.html`、`contents.html` 和 `receipts.json`,凭据必须列出各原始 URL、文件、字节数、SHA256、`http_status=200` 和带时区 `at`。已恢复的凭据需保留 `at_precision=filesystem_write_time_recovered` 等说明,不能伪造 HTTP 头或请求时间。2026-11-09 保存的两个页面通过哈希校验;独立录用清单 URL 返回 403,失败保留,不绕过或依赖其完成采集。
|
||||
|
||||
2025 命令同样使用目录锁、临时目录、最后写入采集报告。五个 JSONL/manifest 均保持 `partial`,记录摘要缺失数量;它不连接数据库。首次采集需独立核验全部标题/作者/栏目和 source IDs,随后做 PostgreSQL 身份预检、必要的逐条版本审核、原生导入与独立验收。若失败且没有完成报告,应先检查原始响应和错误,再使用新输出目录重跑,不重放已提交批次。代码验证、原始目录盘点和采集成功均不代表已导入生产或最终去重完成。
|
||||
`paper_atlas.collectors.aamas` 是按需运行的采集适配器,通过统一入口 `collect aamas-proceedings` 使用。目前只支持已独立核验的 **AAMAS 2012** HTML 版式;其他年份需先核对官方目录和逐篇结构,再增加适配,不能仅替换年份。
|
||||
|
||||
## 来源与范围
|
||||
|
||||
|
||||
@@ -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。
|
||||
@@ -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', {}, '核验其他会议的公开拒稿及撤稿标签'),
|
||||
|
||||
@@ -136,9 +136,6 @@ def read_pages(cache=None, snapshots=None):
|
||||
|
||||
|
||||
def _collect(year, out, *, cache=None, snapshots=None):
|
||||
if year == 2025:
|
||||
from paper_atlas.collectors.aamas2025 import collect as collect_2025
|
||||
return collect_2025(out, cache=cache, snapshots=snapshots)
|
||||
if year != 2012:
|
||||
raise ValueError('Only the independently reviewed AAMAS 2012 layout is supported')
|
||||
pages, receipts = read_pages(cache, snapshots)
|
||||
@@ -193,8 +190,8 @@ def _collect(year, out, *, cache=None, snapshots=None):
|
||||
|
||||
|
||||
def collect(year, out, *, cache=None, snapshots=None):
|
||||
if year not in (2012, 2025):
|
||||
raise ValueError('Only independently reviewed AAMAS 2012/2025 layouts are supported')
|
||||
if year != 2012:
|
||||
raise ValueError('Only the independently reviewed AAMAS 2012 layout is supported')
|
||||
out = Path(out)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
with (out/'aamas.collect.lock').open('a') as lock:
|
||||
@@ -204,7 +201,7 @@ def collect(year, out, *, cache=None, snapshots=None):
|
||||
|
||||
def main(argv=None, *, prog=None):
|
||||
parser = argparse.ArgumentParser(prog=prog)
|
||||
parser.add_argument('--year', type=int, choices=[2012, 2025], required=True)
|
||||
parser.add_argument('--year', type=int, choices=[2012], required=True)
|
||||
parser.add_argument('--out', required=True)
|
||||
source = parser.add_mutually_exclusive_group(required=True)
|
||||
source.add_argument('--cache')
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
"""Reviewed AAMAS 2025 publisher TOC; HTML metadata only, never paper PDFs."""
|
||||
from collections import Counter, defaultdict
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from paper_atlas.db.submissions import timestamp
|
||||
from paper_atlas.io.html import Document, Node, clean
|
||||
from paper_atlas.io.http import fetch
|
||||
|
||||
ROOT = 'https://www.ifaamas.org/Proceedings/aamas2025/'
|
||||
URLS = {'index': ROOT, 'contents': ROOT+'forms/contents.htm'}
|
||||
TITLE = 'AAMAS 2025: Proceedings of the 2025 International Conference on Autonomous Agents and Multiagent Systems'
|
||||
SECTIONS = {
|
||||
'1': ('Keynote Talks', 'keynote', 'keynote', 3),
|
||||
'2': ('Research Paper Track', 'full_paper', 'proceedings_unspecified', 260),
|
||||
'3': ('Extended Abstracts', 'extended_abstract', 'proceedings_unspecified', 148),
|
||||
'4': ('Blue Sky Ideas', 'blue_sky_paper', 'blue_sky', 12),
|
||||
'5': ('Doctoral Consortium', 'doctoral_abstract', 'doctoral', 25),
|
||||
'6': ('Demo Track', 'demo', 'demo', 22),
|
||||
'7': ('JAAMAS Track', 'journal_presentation', 'jaamas', 9),
|
||||
}
|
||||
COMPETITION_TITLE = '[COMP24] The Automated Negotiating Agents Competition (ANAC) 2024 Challenges and Results'
|
||||
PAPER = re.compile(r'\.\./pdfs/p([0-9]+)\.pdf')
|
||||
|
||||
|
||||
def one(items, label):
|
||||
if len(items) != 1:
|
||||
raise ValueError('Expected one '+label)
|
||||
return items[0]
|
||||
|
||||
|
||||
def parse_contents(html, evidence):
|
||||
doc = Document(html)
|
||||
if [clean(n.text()) for n in doc.root.walk() if n.tag == 'title'] != [TITLE]:
|
||||
raise ValueError('Unexpected AAMAS proceedings title/year')
|
||||
for paragraph in doc.root.walk():
|
||||
if paragraph.tag != 'p' or any(n is not paragraph and n.tag == 'p' for n in paragraph.walk()):
|
||||
continue
|
||||
anchors = [n.attrs['name'] for n in paragraph.walk() if n.tag == 'a' and n.attrs.get('name') in SECTIONS]
|
||||
if anchors and (len(anchors) != 1 or clean(paragraph.text()) != SECTIONS[anchors[0]][0]):
|
||||
raise ValueError('Publisher section heading differs from reviewed layout')
|
||||
section = None
|
||||
headers = []
|
||||
records, excluded, counts, seen = [], [], Counter(), set()
|
||||
for node in doc.root.walk():
|
||||
if node.tag == 'a' and node.attrs.get('name') in SECTIONS:
|
||||
section = node.attrs['name']
|
||||
headers.append(section)
|
||||
if node.tag != 'p' or any(n is not node and n.tag == 'p' for n in node.walk()):
|
||||
continue
|
||||
links = [n for n in node.walk() if n.tag == 'a' and PAPER.fullmatch(n.attrs.get('href', ''))]
|
||||
if not links:
|
||||
continue
|
||||
link = one(links, 'paper link per paragraph')
|
||||
if section not in SECTIONS:
|
||||
raise ValueError('Paper outside a reviewed publisher section')
|
||||
page = int(PAPER.fullmatch(link.attrs['href'])[1])
|
||||
if page in seen:
|
||||
raise ValueError('Repeated publisher paper/page ID')
|
||||
seen.add(page)
|
||||
title = clean(link.text())
|
||||
if not title:
|
||||
raise ValueError('Empty paper title')
|
||||
# Names are outside italicized affiliations and separated by explicit BRs.
|
||||
font = one([n for n in node.walk() if n.tag == 'font' and n.attrs.get('color', '').lower() == '#222222'], 'ordered author block')
|
||||
authors, affiliations = [], []
|
||||
name, affiliation = '', ''
|
||||
def flush():
|
||||
nonlocal name, affiliation
|
||||
if clean(name) or clean(affiliation):
|
||||
if not clean(name):
|
||||
raise ValueError('Affiliation without author')
|
||||
authors.append(clean(name)); affiliations.append(clean(affiliation))
|
||||
name, affiliation = '', ''
|
||||
for child in font.children:
|
||||
if isinstance(child, str):
|
||||
name += child
|
||||
elif child.tag == 'br':
|
||||
flush()
|
||||
elif child.tag == 'i':
|
||||
affiliation += child.text()
|
||||
else:
|
||||
raise ValueError('Unreviewed author markup: '+child.tag)
|
||||
flush()
|
||||
if not authors:
|
||||
raise ValueError('Empty author list')
|
||||
heading, publication_format, track, _ = SECTIONS[section]
|
||||
counts[section] += 1
|
||||
if section == '1':
|
||||
excluded.append({'publisher_page': page, 'title': title, 'authors': authors, 'reason': 'publisher keynote talk, not a research-paper submission'})
|
||||
continue
|
||||
note = 'Explicit publisher section; section topics are not research types.'
|
||||
if track == 'proceedings_unspecified':
|
||||
note = 'Published format is known; individual main/special submission track is not provided.'
|
||||
if page == 3000:
|
||||
if section != '6' or title != COMPETITION_TITLE:
|
||||
raise ValueError('Competition report differs from reviewed publisher entry')
|
||||
publication_format, track = 'competition_report', 'proceedings_unspecified'
|
||||
note = 'Publisher places this competition report in Demo Track; no individual demonstration submission is inferred.'
|
||||
records.append({
|
||||
'source': 'aamas_proceedings', 'external_id': '2025/p'+str(page),
|
||||
'url': URLS['contents']+'#'+section, 'title': title, 'abstract': None,
|
||||
'authors': authors, 'identifiers': {}, 'venue': 'AAMAS', 'venue_aliases': [],
|
||||
'year': 2025, 'track': track, 'status': 'accepted',
|
||||
'observed_at': evidence['observed_at'],
|
||||
'raw_decision': 'listed in official AAMAS 2025 published proceedings '+heading,
|
||||
'tags': [{'namespace': 'aamas_proceedings:publication_format', 'value': publication_format}],
|
||||
'metadata': {'publisher_page': page, 'paper_url': ROOT+'pdfs/p'+str(page)+'.pdf',
|
||||
'section_url': URLS['contents'], 'section_anchor': section,
|
||||
'section_heading': heading, 'publication_format': publication_format,
|
||||
'author_affiliations': affiliations, 'source_evidence': evidence,
|
||||
'track_scope_note': note, 'decision_date_precision': 'not_supplied',
|
||||
'missing_abstract_reason': 'Publisher HTML supplies no abstract; research PDF not downloaded.',
|
||||
'publication_evidence': 'Published proceedings entry, not an original decision note.'}})
|
||||
if headers != list(SECTIONS) or dict(counts) != {k: v[3] for k, v in SECTIONS.items()}:
|
||||
raise ValueError('Publisher section inventory differs from reviewed 2025 layout')
|
||||
if len(records) != 476 or len(excluded) != 3 or sum(r['metadata']['publication_format'] == 'competition_report' for r in records) != 1:
|
||||
raise ValueError('Unexpected publication partition')
|
||||
return records, excluded
|
||||
|
||||
|
||||
def read_pages(cache, snapshots):
|
||||
if bool(cache) == bool(snapshots):
|
||||
raise ValueError('Choose exactly one cache or frozen snapshot directory')
|
||||
frozen = json.loads((Path(snapshots)/'receipts.json').read_text())['receipts'] if snapshots else []
|
||||
pages = {}
|
||||
for key, url in URLS.items():
|
||||
if snapshots:
|
||||
r = one([r for r in frozen if r['url'] == url], 'frozen response receipt')
|
||||
filename = Path(r['file']); base = Path(snapshots).resolve()
|
||||
if filename.is_absolute() or '..' in filename.parts or not (base/filename).resolve().is_relative_to(base) or r.get('http_status') != 200:
|
||||
raise ValueError('Unsafe or unsuccessful publisher response')
|
||||
data = (base/filename).read_bytes()
|
||||
if len(data) > 8*1024**2 or len(data) != r['bytes'] or hashlib.sha256(data).hexdigest() != r['sha256']:
|
||||
raise ValueError('Frozen publisher response checksum/size mismatch')
|
||||
observed = r['at']
|
||||
else:
|
||||
data = fetch(url, cache, 8*1024**2, delay=1)
|
||||
r = json.loads((Path(cache)/hashlib.sha256(url.encode()).hexdigest()).with_suffix('.json').read_text())
|
||||
observed = r['retrieved_at']
|
||||
timestamp(observed)
|
||||
pages[key] = data, {'url': url, 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
|
||||
'file': 'evidence/'+hashlib.sha256(data).hexdigest()+'.html',
|
||||
'observed_at': observed, 'http_status': 200, 'response_receipt': r}
|
||||
return pages
|
||||
|
||||
|
||||
def collect(out, *, cache=None, snapshots=None):
|
||||
pages = read_pages(cache, snapshots)
|
||||
if [clean(n.text()) for n in Document(pages['index'][0].decode('utf-8')).root.walk() if n.tag == 'title'] != [TITLE]:
|
||||
raise ValueError('Unexpected AAMAS 2025 introduction')
|
||||
records, excluded = parse_contents(pages['contents'][0].decode('utf-8'), pages['contents'][1])
|
||||
grouped = defaultdict(list)
|
||||
for row in records: grouped[row['track']].append(row)
|
||||
out = Path(out); out.mkdir(parents=True, exist_ok=True); files = []
|
||||
with tempfile.TemporaryDirectory(prefix='.aamas2025-', dir=out) as temporary:
|
||||
stage = Path(temporary); (stage/'evidence').mkdir()
|
||||
for data, e in pages.values(): (stage/e['file']).write_bytes(data)
|
||||
for track, group in sorted(grouped.items()):
|
||||
p = stage/f'aamas-proceedings-2025-{track}.jsonl'
|
||||
p.write_text(''.join(json.dumps(r, ensure_ascii=False)+'\n' for r in group))
|
||||
m = {'source': 'aamas_proceedings', 'parser_version': 2, 'venue': 'AAMAS', 'year': 2025,
|
||||
'track': track, 'count': len(group), 'file': str(out/p.name), 'file_sha256': hashlib.sha256(p.read_bytes()).hexdigest(),
|
||||
'list_url': URLS['contents'], 'status': 'partial', 'missing_abstract_count': len(group),
|
||||
'publication_format_counts': dict(Counter(r['metadata']['publication_format'] for r in group)),
|
||||
'note': 'Complete published TOC partition; HTML abstracts absent, identities and submission-track scope require independent review.'}
|
||||
p.with_suffix('.manifest.json').write_text(json.dumps(m, ensure_ascii=False, indent=2)+'\n'); files.append(m)
|
||||
result = {'source': 'aamas_proceedings', 'year': 2025, 'finished_at': timestamp(),
|
||||
'status': 'collected_pending_identity_review', 'records': len(records), 'files': files,
|
||||
'excluded_publications': excluded, 'source_receipts': [e for _, e in pages.values()],
|
||||
'publication_format_counts': dict(Counter(r['metadata']['publication_format'] for r in records)),
|
||||
'database_written': False, 'pdfs_downloaded': False,
|
||||
'note': 'Neither absence from proceedings nor aggregate rejection/withdrawal counts identify an individual negative decision.'}
|
||||
report = 'aamas-proceedings-2025.json'; (stage/report).write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n')
|
||||
(out/'evidence').mkdir(exist_ok=True)
|
||||
for p in (stage/'evidence').iterdir(): p.replace(out/'evidence'/p.name)
|
||||
for p in stage.iterdir():
|
||||
if p.is_file() and p.name != report: p.replace(out/p.name)
|
||||
(stage/report).replace(out/report)
|
||||
return result
|
||||
@@ -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))
|
||||
-4281
File diff suppressed because it is too large
Load Diff
-9968
File diff suppressed because it is too large
Load Diff
Vendored
-54
@@ -1,54 +0,0 @@
|
||||
<HTML><HEAD><TITLE>AAMAS 2025: Proceedings of the 2025 International Conference on Autonomous Agents and Multiagent Systems</TITLE>
|
||||
<META http-equiv=Content-Type content="text/html; charset=utf-8"><LINK
|
||||
href="forms/index_files/factemplate.css" type=text/css rel=stylesheet>
|
||||
<META content="MSHTML 6.00.2900.3562" name=GENERATOR></HEAD>
|
||||
<BODY bgcolor="#010101" bgproperties="fixed"><DIV style="TEXT-ALIGN: center" >
|
||||
<table width="724" border="0" align="center" cellpadding="0" cellspacing="0" valign="top">
|
||||
<td colspan="2" align="center" valign="top" bgcolor="#FFFFFF"><img src="forms/index_files/aamas25.png" width="750" height="300" alt=""/></td>
|
||||
</tr>
|
||||
<tr bgcolor="#FFFFFF">
|
||||
<td width="125" align="center" valign="top" bgcolor="#FFFFFF"><table width="125" border="1" align="center" cellpadding="1" cellspacing="1" bordercolor="#444444" style="margin-bottom: 2">
|
||||
<tr>
|
||||
<td bgcolor="#FFFFFF"><p style="text-indent: 0; margin-left: 6; margin-right: 6; margin-top: 5; margin-bottom: 4" align="left"><A href="forms/index.htm">Main
|
||||
Page</A></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#FFFFFF"><p style="text-indent: 0; margin-left: 6; margin-right: 6; margin-top: 5; margin-bottom: 4" align="left"><A href="forms/contents.htm">Table
|
||||
of Contents</A></p></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
<td width="599"><p style="text-indent: 0; margin-left: 12; margin-right: 14; margin-top: 12; margin-bottom: 12" align="left"><font color="#010101" size="4" face="Arial, Helvetica, sans-serif"><strong>Welcome
|
||||
to the Proceedings of AAMAS-2025</strong></font></p>
|
||||
<p style="text-indent: 0; margin-left: 12; margin-right: 14; margin-top: 12; margin-bottom: 12;" align="left"><font color="#333333" size="2" face="Times New Roman, Times, serif">These
|
||||
are the proceedings of the 24th International Conference on Autonomous
|
||||
Agents and Multiagent Systems (AAMAS-2025). They are published by the
|
||||
International Foundation for Autonomous Agents and Multiagent Systems
|
||||
(IFAAMAS).</font></p></td>
|
||||
</tr>
|
||||
<tr bgcolor="#FFFFFF">
|
||||
<td align="center" valign="middle" bgcolor="#FFFFFF"> </td>
|
||||
<td><table width="96%" border="0" cellspacing="2" cellpadding="2">
|
||||
<tr align="left" valign="top">
|
||||
<td> <p style="text-indent: 0; margin-left: 12; margin-right: 6; margin-top: 0; margin-bottom: 6;" align="left"><font color="#333333" face="Times New Roman, Times, serif"><font size="2"><strong>ISBN: 979-8-4007-1426-9</strong></font></font></p></td>
|
||||
<td> <p style="text-indent: 0; margin-left: 12; margin-right: 6; margin-top: 0; margin-bottom: 6;" align="left"><font color="#333333" face="Times New Roman, Times, serif"></font></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="42%"><div align="left"><img src="forms/index_files/ifaamas.jpg" width="256" height="104"></div></td>
|
||||
<td width="58%"><div align="center"><img src="forms/index_files/ACM-In-Cooperation_medium.jpg" width="285" height="86"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="text-indent: 0; margin-left: 12; margin-right: 14; margin-top: 12; margin-bottom: 12;" align="left"><font color="#333333" size="2" face="Times New Roman, Times, serif">Copyright
|
||||
© 2025 by International Foundation for Autonomous Agents and Multiagent
|
||||
Systems (IFAAMAS). Permission to make digital or hard copies of portions
|
||||
of this work for personal or classroom use is granted without fee provided
|
||||
that copies are not made or distributed for profit or commercial advantage
|
||||
and that copies bear this notice and the full citation on the first page.
|
||||
Copyright for components of this work owned by others than IFAAMAS must
|
||||
be honored. Abstracting with credit is permitted. To copy otherwise, to
|
||||
republish, to post on servers or to redistribute to lists, requires prior
|
||||
specific permission and/or a fee. </font></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p> </p>
|
||||
|
||||
</BODY></HTML>
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"status": "two_successful_official_metadata_pages_recovered_third403_not_retried",
|
||||
"receipts": [
|
||||
{
|
||||
"file": "index.html",
|
||||
"url": "https://www.ifaamas.org/Proceedings/aamas2025/",
|
||||
"bytes": 4107,
|
||||
"sha256": "850b7825c66d0147ac8a8bbf03a554590e4a9e8f4b5c576e043999bbd43317f4",
|
||||
"at": "2026-11-09T12:31:49.347577+00:00",
|
||||
"at_precision": "filesystem_write_time_recovered",
|
||||
"http_status": 200,
|
||||
"content_type": "text/html",
|
||||
"receipt_recovered": true,
|
||||
"scope": "Collectorasserted200/htmlbeforewrite;request3acceptedprogram403;noheaders/finalredirectURLpersisted;no refetch or rawbytesmutation."
|
||||
},
|
||||
{
|
||||
"file": "contents.html",
|
||||
"url": "https://www.ifaamas.org/Proceedings/aamas2025/forms/contents.htm",
|
||||
"bytes": 370650,
|
||||
"sha256": "732847b51972f34c721a49d8bd963a80a029a48f3ec08022c9a0d5ddc2945549",
|
||||
"at": "2026-11-09T12:31:52.006642+00:00",
|
||||
"at_precision": "filesystem_write_time_recovered",
|
||||
"http_status": 200,
|
||||
"content_type": "text/html",
|
||||
"receipt_recovered": true,
|
||||
"scope": "Collectorasserted200/htmlbeforewrite;request3acceptedprogram403;noheaders/finalredirectURLpersisted;no refetch or rawbytesmutation."
|
||||
}
|
||||
],
|
||||
"failed_url": "https://aamas2025.org/index.php/conference/program/accepted-papers/",
|
||||
"not_attempted_url": "https://aamas2025.org/index.php/conference/calls/submission-instructions-main-technical-track/",
|
||||
"original_error": {
|
||||
"error": "HTTP Error 403: Forbidden",
|
||||
"type": "HTTPError",
|
||||
"downloaded": 2
|
||||
},
|
||||
"research_fulltext_downloaded": false,
|
||||
"database_written": false
|
||||
}
|
||||
Vendored
+4356
File diff suppressed because it is too large
Load Diff
+1501
File diff suppressed because it is too large
Load Diff
Vendored
+1646
File diff suppressed because it is too large
Load Diff
Vendored
+1585
File diff suppressed because it is too large
Load Diff
Vendored
+148
@@ -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>
|
||||
Vendored
+155
@@ -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>
|
||||
Vendored
+65
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from paper_atlas.collectors import aamas, aamas2025
|
||||
|
||||
|
||||
FIXTURE = Path(__file__).parent/'fixtures/aamas2025'
|
||||
|
||||
|
||||
class Aamas2025Tests(unittest.TestCase):
|
||||
def parse(self, html=None):
|
||||
return aamas2025.parse_contents(html or (FIXTURE/'contents.html').read_text(),
|
||||
{'observed_at': '2026-11-09T12:31:50+00:00', 'url': aamas2025.URLS['contents']})
|
||||
|
||||
def test_real_toc_preserves_every_title_author_and_affiliation(self):
|
||||
expected = json.loads((FIXTURE/'expected-authors.json').read_text())
|
||||
self.assertEqual(hashlib.sha256((FIXTURE/'contents.html').read_bytes()).hexdigest(), expected['source_sha256'])
|
||||
rows, excluded = self.parse()
|
||||
self.assertEqual((len(rows), len(excluded)), (476, 3))
|
||||
by_url = {r['paper_url']: r for r in expected['entries']}
|
||||
for row in rows:
|
||||
original = by_url[row['metadata']['paper_url']]
|
||||
self.assertEqual(row['title'], original['title'])
|
||||
self.assertEqual(row['authors'], [a['name'] for a in original['authors']])
|
||||
self.assertEqual(row['metadata']['author_affiliations'], [a['affiliation'] for a in original['authors']])
|
||||
self.assertIsNone(row['abstract'])
|
||||
self.assertEqual(row['identifiers'], {})
|
||||
self.assertEqual(row['status'], 'accepted')
|
||||
self.assertNotIn('decision_date', row)
|
||||
self.assertTrue(all(t['namespace'] == 'aamas_proceedings:publication_format' for t in row['tags']))
|
||||
|
||||
def test_publication_format_and_track_are_separate(self):
|
||||
rows, excluded = self.parse()
|
||||
groups = {k: sum(r['track'] == k for r in rows) for k in {r['track'] for r in rows}}
|
||||
self.assertEqual(groups, {'proceedings_unspecified': 409, 'blue_sky': 12, 'doctoral': 25, 'demo': 21, 'jaamas': 9})
|
||||
competition = next(r for r in rows if r['external_id'] == '2025/p3000')
|
||||
self.assertEqual(competition['year'], 2025)
|
||||
self.assertEqual(competition['metadata']['publication_format'], 'competition_report')
|
||||
self.assertEqual(competition['metadata']['section_heading'], 'Demo Track')
|
||||
self.assertEqual(competition['track'], 'proceedings_unspecified')
|
||||
self.assertEqual(len({r['external_id'] for r in rows}), 476)
|
||||
self.assertEqual([r['publisher_page'] for r in excluded], [1, 3, 4])
|
||||
|
||||
def test_refuses_changed_edition_inventory_or_author_markup(self):
|
||||
original = (FIXTURE/'contents.html').read_text()
|
||||
for html in [original.replace('AAMAS 2025:', 'AAMAS 2026:', 1),
|
||||
original.replace('../pdfs/p5.pdf', '../pdfs/p14.pdf', 1),
|
||||
original.replace('Paper Track</font>', 'Workshop Track</font>'),
|
||||
original.replace('Amir Abolfazli', '', 1),
|
||||
original.replace('[COMP24]', '[CHANGED]', 1)]:
|
||||
with self.subTest(change=html[:60]):
|
||||
with self.assertRaises(ValueError): self.parse(html)
|
||||
|
||||
def test_frozen_collection_is_partial_and_never_fetches_pdfs(self):
|
||||
with tempfile.TemporaryDirectory() as tmp, patch.object(aamas2025, 'fetch', side_effect=AssertionError('No HTTP in frozen mode')):
|
||||
out = Path(tmp)/'out'
|
||||
report = aamas.collect(2025, out, snapshots=FIXTURE)
|
||||
self.assertEqual(report['records'], 476)
|
||||
self.assertFalse(report['database_written'])
|
||||
self.assertFalse(report['pdfs_downloaded'])
|
||||
self.assertEqual(len(report['files']), 5)
|
||||
for m in report['files']:
|
||||
self.assertEqual(m['status'], 'partial')
|
||||
self.assertEqual(m['missing_abstract_count'], m['count'])
|
||||
self.assertEqual(hashlib.sha256(Path(m['file']).read_bytes()).hexdigest(), m['file_sha256'])
|
||||
receipt = report['source_receipts'][0]['response_receipt']
|
||||
self.assertEqual(receipt['at_precision'], 'filesystem_write_time_recovered')
|
||||
self.assertTrue((out/'aamas-proceedings-2025.json').is_file())
|
||||
|
||||
def test_rejects_tampered_or_unsafe_frozen_response(self):
|
||||
import shutil
|
||||
for mode in ['tamper', 'path', 'status']:
|
||||
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp); source = root/'source'; shutil.copytree(FIXTURE, source)
|
||||
p = source/'receipts.json'; r = json.loads(p.read_text())
|
||||
if mode == 'tamper': (source/'contents.html').write_text('bad')
|
||||
elif mode == 'path': r['receipts'][0]['file'] = '../index.html'
|
||||
else: r['receipts'][0]['http_status'] = 403
|
||||
p.write_text(json.dumps(r))
|
||||
with self.assertRaises(ValueError): aamas.collect(2025, root/'out', snapshots=source)
|
||||
self.assertFalse((root/'out/aamas-proceedings-2025.json').exists())
|
||||
@@ -99,7 +99,7 @@ class AamasProceedingsTests(unittest.TestCase):
|
||||
aamas.collect(2012, root/'out', snapshots=source)
|
||||
self.assertFalse((root/'out/aamas-proceedings-2012.json').exists())
|
||||
with self.assertRaises(ValueError):
|
||||
aamas.collect(2026, root/'future', snapshots=source)
|
||||
aamas.collect(2025, root/'future', snapshots=source)
|
||||
self.assertFalse((root/'future').exists())
|
||||
|
||||
def test_ids_colliding_across_sections_do_not_produce_a_completed_collection(self):
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user