Skip to content
Merged

Main #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,19 @@ python -m rrparser --dataset rhea --diameters '<=8' -o rules.csv
python -m rrparser --dataset rhea --diameters '>=8' -o rules.csv
```

`--radii` supports the same expressions over integer radii 0–10:
`-r` / `--radii` supports the same expressions over integer radii 0–10:
`0,1,2`, `0-4`, `<4`, `<=4`, `>4`, `>=4`, and combinations such as `0-2,>=8`.
For example, `--radii '<=4'` is equivalent to `--diameters '<=8'`.
The two options are mutually exclusive. Both are expanded into normalized
integer radii by the argument parser, before calling `parse_rules()`.
Without either option, all radii 0–10 (diameters 0,2,…,20) are selected.

For templates, a row is retained when at least one source radius matches
`diameter / 2`, once per template. The output `RADII` column retains all source
radii, separated by semicolons. Explicit odd diameters or values above 20 are
`diameter / 2`, once per template. The output `RADII` column retains only the
selected radii, separated by semicolons; `RADIUS_MIN` and `RADIUS_MAX` are
updated to match. For example, `-r '>4'` exports only radii 5–10 even if a
matching source template also covers radius 4. Cached sources are unchanged.
Explicit odd diameters or values above 20 are
rejected before parsing, including for rr02.

rr02 supplies diameters 2,4,…,16. The extended default therefore selects all
Expand Down Expand Up @@ -161,7 +164,7 @@ archive cache.
| `--list-releases` | Print available release/dataset pairs and exit. |
| `--cache-dir`, `--rules-dir` | Source cache directory; default package-local `rrparser/cache`. |
| `-d`, `--diameters` | List, range, or comparison; default all even diameters 0–20. |
| `--radii` | List, range, or comparison over radii 0–10, as an alternative to `--diameters`. |
| `-r`, `--radii` | List, range, or comparison over radii 0–10, as an alternative to `--diameters`. |
| `-rt`, `--rule-type` | `all` (default), `retro`, or `forward`; templates require `all`. |
| `-ec`, `--ec` | File of comma-separated EC prefixes to keep. |
| `-ecx`, `--ecx` | File of comma-separated EC prefixes to exclude; mutually exclusive with `--ec`. |
Expand Down
1 change: 1 addition & 0 deletions rrparser/Args.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def _add_arguments(parser):
"For templates, diameter = 2 * radius; rr02 provides diameters 2 to 16",
)
sizes.add_argument(
"-r",
"--radii",
type=parse_radii,
help='Radii: list (0,1,2), inclusive range (0-4), "<4", "<=4", ">4" or ">=4". '
Expand Down
19 changes: 14 additions & 5 deletions rrparser/Parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from re import split
from tarfile import open as open_tar

from pandas import DataFrame, concat, read_csv

Check notice on line 17 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unsatisfied package requirements

Package containing module 'pandas' is not listed in the project requirements

from .Args import (
DEFAULT_RADII,
Expand Down Expand Up @@ -97,7 +97,7 @@
}
for column in ("REACTIONS", "ECS", "DATASETS", "RADII"):
if column in aggregations:
aggregations[column] = union

Check warning on line 100 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect type

Expected type 'str' (matched generic type '_VT'), got '(values: {dropna}) -\> LiteralString' instead
aggregations["SCORE"] = "max"
if "VALID" in aggregations:
aggregations["VALID"] = "all"
Expand All @@ -115,7 +115,7 @@
for column, position in (("RADIUS_MIN", 0), ("RADIUS_MAX", -1)):
if column in merged.columns:
merged[column] = radii.map(
lambda values, position=position: (

Check notice on line 118 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Shadowing names from outer scopes

Shadows name 'position' from outer scope

Check warning on line 118 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unbound local variables

Local variable 'position' might be referenced before assignment
values[position] if values else None
)
)
Expand Down Expand Up @@ -208,7 +208,7 @@
"SCORE": "Score",
}
)
results = filter_(rf, rule_type, radii, ecx, ec, logger=logger)

Check warning on line 211 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect type

Expected type 'list\[int\]', got 'str' instead

# If 'rules_scores_file' is set, then read it and merge with 'results'
if scores_file:
Expand Down Expand Up @@ -299,7 +299,7 @@
ecx = ecx if ecx is not None else []
ec = ec if ec is not None else []
ec_filter = ecx if ecx else ec
filter = "in" if ec else "not in"

Check notice on line 302 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Shadowing built-in names

Shadows built-in name 'filter'
logger.info("Filtering rules:")
logger.info(f" -> EC numbers ({filter}): {ec_filter}")
logger.info(f" -> Radii: {radii}")
Expand All @@ -320,16 +320,18 @@
raise ValueError(
"RetroRules templates have no retro/forward annotation; use --rule-type all"
)
radius_idx = df[
selection = set(radii)
selected_radii = (
df["RADII"]
.fillna("")
.astype(str)
.apply(
lambda values: any(
int(radius) in radii for radius in values.split(";") if radius
.map(
lambda values: sorted(
{int(radius) for radius in values.split(";") if radius} & selection
)
)
].index
)
radius_idx = df[selected_radii.map(bool)].index
query_vars["radius_idx"] = radius_idx
query += " & index in @radius_idx"
else:
Expand All @@ -340,6 +342,13 @@
logger.debug(f"Query: {query}")
try:
_df = df.query(query, local_dict=query_vars)
if "RADII" in _df.columns:
_df = _df.copy()
retained_radii = selected_radii.loc[_df.index]

Check warning on line 347 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unbound local variables

Local variable 'selected_radii' might be referenced before assignment
_df["RADII"] = retained_radii.map(lambda values: ";".join(map(str, values)))
for column, bound in (("RADIUS_MIN", min), ("RADIUS_MAX", max)):
if column in _df.columns:
_df[column] = retained_radii.map(bound)
logger.info("")
logger.info(f"=> {len(_df)} rules found")
logger.info("")
Expand All @@ -349,7 +358,7 @@


def filter_rules_by_ec(
df: DataFrame, ec_filter: list[str], logger: Logger = LOGGER

Check notice on line 361 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect docstring

Missing parameter ec_filter in docstring

Check notice on line 361 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect docstring

Missing parameter ec_filter in docstring
) -> list[int]:
"""
Filter out rules that contain at least one EC number that starts with one in 'ecx'.
Expand All @@ -358,7 +367,7 @@
----------
df: DataFrame
Pandas dataframe
ec: List[str]

Check notice on line 370 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect docstring

Unexpected parameter ec in docstring

Check notice on line 370 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect docstring

Unexpected parameter ec in docstring
List of EC numbers to filter
logger : Logger
The logger object.
Expand All @@ -367,7 +376,7 @@
ec_numbers = df["EC number"].fillna("").str.split(";")

# Find indices of rows where any EC number starts with one in 'ec_filter'
def filter_ec_numbers(ec_list, ec_filter):

Check notice on line 379 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Shadowing names from outer scopes

Shadows name 'ec_filter' from outer scope
return any(any(ec.startswith(ec_f) for ec_f in ec_filter) for ec in ec_list)

# Apply filtering function and get indices
Expand Down Expand Up @@ -396,9 +405,9 @@

def fetch_retrorules(
rules_dir: str = DEFAULT_RULES_DIR,
logger: Logger = LOGGER,

Check notice on line 408 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'logger' value is not used
release: str = DEFAULT_RELEASE,

Check notice on line 409 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect docstring

Missing parameter release in docstring
dataset: str = "metanetx",

Check notice on line 410 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect docstring

Missing parameter dataset in docstring

Check notice on line 410 in rrparser/Parser.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect docstring

Missing parameter dataset in docstring
) -> str:
"""
Fetch RetroRules over Internet if not already on disk.
Expand Down
3 changes: 3 additions & 0 deletions tests/test_aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ def test_cached_datasets_are_merged_before_filtering(tmp_path, selection):
assert shared["RADIUS_MIN"] == 0
assert shared["RADIUS_MAX"] == 10
assert pd.read_csv(StringIO(filtered))["Rule ID"].tolist() == ["shared"]
filtered_row = pd.read_csv(StringIO(filtered), dtype={"RADII": str}).iloc[0]
assert filtered_row["RADII"] == "10"
assert filtered_row["RADIUS_MIN"] == filtered_row["RADIUS_MAX"] == 10
assert [source.read_bytes() for source in sources] == originals


Expand Down
41 changes: 41 additions & 0 deletions tests/test_radii.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def test_radius_expressions_select_templates(tmp_path, expression, expected):
[
("--radii", "0-4", [0, 1, 2, 3, 4]),
("--radii", "<=4", [0, 1, 2, 3, 4]),
("-r", ">4", [5, 6, 7, 8, 9, 10]),
("--diameters", "0-8", [0, 1, 2, 3, 4]),
("-d", "<=8", [0, 1, 2, 3, 4]),
("--radii", ">=8", [8, 9, 10]),
Expand Down Expand Up @@ -108,3 +109,43 @@ def test_defaults_and_empty_selection():
args = build_args_parser().parse_args([])
assert list(args.radii) == list(range(11))
assert parse_diameters("<0") == parse_radii("<0") == []


@pytest.mark.parametrize(
"option,expression,expected",
[
("-r", ">4", "5;6;7;8;9;10"),
("--radii", ">=4", "4;5;6;7;8;9;10"),
("-r", "4-6", "4;5;6"),
("-r", "0,10", "10"),
("-d", ">8", "5;6;7;8;9;10"),
("-r", ">10", ""),
],
)
def test_exported_radius_metadata_matches_selection(
tmp_path, option, expression, expected
):
source = tmp_path / "templates.csv"
pd.DataFrame(
{
"TEMPLATE_ID": ["RR:shared"],
"TEMPLATE": ["[C]>>[O]"],
"SCORE": [1.0],
"ECS": ["1.1.1.1"],
"RADII": ["4;5;6;7;8;9;10"],
"RADIUS_MIN": [4],
"RADIUS_MAX": [10],
}
).to_csv(source, index=False)
original = source.read_bytes()
args = build_args_parser().parse_args([option, expression])
result = parse_rules(None, rules_file=str(source), radii=args.radii)
output = pd.read_csv(StringIO(result), dtype={"RADII": str})
if expected:
assert output["RADII"].tolist() == [expected]
radii = list(map(int, expected.split(";")))
assert output["RADIUS_MIN"].tolist() == [min(radii)]
assert output["RADIUS_MAX"].tolist() == [max(radii)]
else:
assert output.empty
assert source.read_bytes() == original
Loading