Skip to content

Commit bfffbc3

Browse files
committed
Add transport bipolar-risk screening
1 parent 0b833ad commit bfffbc3

11 files changed

Lines changed: 129 additions & 12 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![CI](https://img.shields.io/github/actions/workflow/status/chatmaterials/transport-analysis/ci.yml?branch=main&label=CI)](https://github.com/chatmaterials/transport-analysis/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/chatmaterials/transport-analysis?display_name=tag)](https://github.com/chatmaterials/transport-analysis/releases)
44

5-
Standalone skill for transport-relevant DFT result analysis, including thermoelectric-style screening descriptors and batch candidate ranking.
5+
Standalone skill for transport-relevant DFT result analysis, including thermoelectric-style screening descriptors, bipolar-risk checks, and batch candidate ranking.
66

77
## Install
88

@@ -16,9 +16,10 @@ npx skills add chatmaterials/transport-analysis -g -y
1616
python3 -m py_compile scripts/*.py
1717
npx skills add . --list
1818
python3 scripts/analyze_carrier_type.py fixtures/band/bands.dat --occupied-bands 2 --fermi 0.35 --json
19+
python3 scripts/analyze_bipolar_risk.py fixtures/band/bands.dat --occupied-bands 2 --fermi 0.35 --temperature-k 300 --json
1920
python3 scripts/analyze_effective_mass.py fixtures/effective_mass/effective_mass.dat --json
20-
python3 scripts/analyze_transport_trend.py --band-path fixtures/band/bands.dat --dos-path fixtures/dos/dos.dat --occupied-bands 2 --fermi 0.35 --json
21-
python3 scripts/compare_transport_candidates.py fixtures fixtures/candidates/heavy fixtures/candidates/metallic --occupied-bands 2 --fermi 0.35 --target-gap-min 0.5 --target-gap-max 1.0 --prefer-carrier electron-like --json
22-
python3 scripts/export_transport_report.py --band-path fixtures/band/bands.dat --dos-path fixtures/dos/dos.dat --mass-path fixtures/effective_mass/effective_mass.dat --occupied-bands 2 --fermi 0.35
21+
python3 scripts/analyze_transport_trend.py --band-path fixtures/band/bands.dat --dos-path fixtures/dos/dos.dat --occupied-bands 2 --fermi 0.35 --temperature-k 300 --json
22+
python3 scripts/compare_transport_candidates.py fixtures fixtures/candidates/heavy fixtures/candidates/narrow-gap fixtures/candidates/metallic --occupied-bands 2 --fermi 0.35 --target-gap-min 0.5 --target-gap-max 1.0 --prefer-carrier electron-like --temperature-k 300 --json
23+
python3 scripts/export_transport_report.py --band-path fixtures/band/bands.dat --dos-path fixtures/dos/dos.dat --mass-path fixtures/effective_mass/effective_mass.dat --occupied-bands 2 --fermi 0.35 --temperature-k 300
2324
python3 scripts/run_regression.py
2425
```

SKILL.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: "transport-analysis"
3-
description: "Use when the task is to analyze transport-relevant quantities from DFT-derived results, including carrier-type tendency, effective-mass estimates, simple DOS-informed transport trends, thermoelectric-style screening descriptors, multi-candidate ranking, and compact markdown reports from finished calculations."
3+
description: "Use when the task is to analyze transport-relevant quantities from DFT-derived results, including carrier-type tendency, effective-mass estimates, simple DOS-informed transport trends, thermoelectric-style screening descriptors, bipolar-risk checks, multi-candidate ranking, and compact markdown reports from finished calculations."
44
---
55

66
# Transport Analysis
@@ -13,6 +13,7 @@ Use this skill for transport-oriented post-processing rather than generic workfl
1313
- estimate an effective mass from a simple band-edge dispersion
1414
- summarize a transport trend from band-edge and DOS information
1515
- derive compact thermoelectric-style screening descriptors such as activation energy and transport quality score
16+
- estimate bipolar-conduction risk from the band gap and temperature
1617
- rank multiple candidate datasets with a compact screening heuristic
1718
- write a compact transport-analysis report from existing data
1819

@@ -24,6 +25,8 @@ Use this skill for transport-oriented post-processing rather than generic workfl
2425
Estimate an effective mass from a simple band-edge dispersion.
2526
- `scripts/analyze_transport_trend.py`
2627
Summarize a simple transport trend from band-edge and DOS information and derive compact quality descriptors.
28+
- `scripts/analyze_bipolar_risk.py`
29+
Estimate a compact bipolar-conduction risk from the band gap and temperature.
2730
- `scripts/compare_transport_candidates.py`
2831
Rank multiple candidate datasets with a compact gap-plus-mass screening heuristic.
2932
- `scripts/export_transport_report.py`
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
0.00 -5.00 0.05 0.20 2.30
2+
0.50 -4.95 0.10 0.25 2.25
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-1.0 0.5
2+
-0.5 0.7
3+
0.0 0.0
4+
0.5 0.4
5+
1.0 0.8
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-0.20 0.006
2+
-0.10 0.0015
3+
0.00 0.0000
4+
0.10 0.0015
5+
0.20 0.006

references/transport.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
- DOS-informed transport trend summaries are qualitative, not a replacement for full transport coefficient calculations.
66
- Screening scores are useful for ranking, but they are not a substitute for full transport coefficients.
77
- Activation-energy and quality-score style descriptors are useful for screening, but they remain compact proxies rather than predictive transport models.
8+
- Bipolar-risk estimates are useful for narrow-gap screening, but they remain simplified intrinsic-excitation proxies rather than full finite-temperature transport solutions.

scripts/analyze_bipolar_risk.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import json
7+
import math
8+
from pathlib import Path
9+
10+
from analyze_carrier_type import analyze as analyze_carrier
11+
12+
13+
KB_EV_K = 8.617333262145e-5
14+
15+
16+
def classify_risk(score: float) -> str:
17+
if score < 1e-4:
18+
return "low-bipolar-risk"
19+
if score < 1e-2:
20+
return "moderate-bipolar-risk"
21+
return "high-bipolar-risk"
22+
23+
24+
def analyze(path: Path, occupied_bands: int, fermi: float, temperature_k: float) -> dict[str, object]:
25+
carrier = analyze_carrier(path, occupied_bands, fermi)
26+
gap = float(carrier["band_gap_eV"])
27+
if temperature_k <= 0:
28+
raise SystemExit("Temperature must be positive")
29+
score = math.exp(-gap / (2.0 * KB_EV_K * temperature_k))
30+
return {
31+
"path": str(path),
32+
"temperature_K": temperature_k,
33+
"band_gap_eV": gap,
34+
"bipolar_risk_score": score,
35+
"bipolar_risk_class": classify_risk(score),
36+
"observations": [
37+
"Bipolar risk was estimated from a simple intrinsic excitation factor based on the band gap and temperature."
38+
],
39+
}
40+
41+
42+
def main() -> None:
43+
parser = argparse.ArgumentParser(description="Estimate a compact bipolar-conduction risk from the band gap and temperature.")
44+
parser.add_argument("path")
45+
parser.add_argument("--occupied-bands", type=int, default=2)
46+
parser.add_argument("--fermi", type=float, required=True)
47+
parser.add_argument("--temperature-k", type=float, default=300.0)
48+
parser.add_argument("--json", action="store_true")
49+
args = parser.parse_args()
50+
payload = analyze(
51+
Path(args.path).expanduser().resolve(),
52+
args.occupied_bands,
53+
args.fermi,
54+
args.temperature_k,
55+
)
56+
if args.json:
57+
print(json.dumps(payload, indent=2))
58+
return
59+
print(json.dumps(payload, indent=2))
60+
61+
62+
if __name__ == "__main__":
63+
main()

scripts/analyze_transport_trend.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
from pathlib import Path
88

9+
from analyze_bipolar_risk import analyze as analyze_bipolar
910
from analyze_carrier_type import analyze as analyze_carrier
1011
from analyze_effective_mass import analyze as analyze_mass
1112

@@ -23,8 +24,9 @@ def analyze_dos(path: Path) -> dict[str, float]:
2324
return {"dos_at_fermi": nearest[1]}
2425

2526

26-
def analyze(band_path: Path, dos_path: Path, occupied_bands: int, fermi: float, mass_path: Path | None) -> dict[str, object]:
27+
def analyze(band_path: Path, dos_path: Path, occupied_bands: int, fermi: float, mass_path: Path | None, temperature_k: float = 300.0) -> dict[str, object]:
2728
carrier = analyze_carrier(band_path, occupied_bands, fermi)
29+
bipolar = analyze_bipolar(band_path, occupied_bands, fermi, temperature_k)
2830
dos = analyze_dos(dos_path)
2931
mass = analyze_mass(mass_path) if mass_path else None
3032
if dos["dos_at_fermi"] < 1e-6:
@@ -58,12 +60,15 @@ def analyze(band_path: Path, dos_path: Path, occupied_bands: int, fermi: float,
5860
"band_gap_eV": carrier["band_gap_eV"],
5961
"activation_energy_eV": carrier["activation_energy_eV"],
6062
"dos_at_fermi": dos["dos_at_fermi"],
63+
"temperature_K": temperature_k,
6164
"regime": regime,
6265
"effective_mass_me": mass["effective_mass_me"] if mass else None,
6366
"mobility_hint": mobility_hint,
6467
"thermoelectric_quality_score": quality_score,
6568
"screening_class": screening_class,
6669
"dopability_hint": dopability_hint,
70+
"bipolar_risk_score": bipolar["bipolar_risk_score"],
71+
"bipolar_risk_class": bipolar["bipolar_risk_class"],
6772
"observations": ["Transport tendency summarized from band-edge position, DOS at the Fermi level, and optional effective mass."],
6873
}
6974

@@ -75,6 +80,7 @@ def main() -> None:
7580
parser.add_argument("--mass-path")
7681
parser.add_argument("--occupied-bands", type=int, default=2)
7782
parser.add_argument("--fermi", type=float, required=True)
83+
parser.add_argument("--temperature-k", type=float, default=300.0)
7884
parser.add_argument("--json", action="store_true")
7985
args = parser.parse_args()
8086
payload = analyze(
@@ -83,6 +89,7 @@ def main() -> None:
8389
args.occupied_bands,
8490
args.fermi,
8591
Path(args.mass_path).expanduser().resolve() if args.mass_path else None,
92+
args.temperature_k,
8693
)
8794
if args.json:
8895
print(json.dumps(payload, indent=2))

scripts/compare_transport_candidates.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
from pathlib import Path
88

9+
from analyze_bipolar_risk import analyze as analyze_bipolar
910
from analyze_carrier_type import analyze as analyze_carrier
1011
from analyze_effective_mass import analyze as analyze_mass
1112
from analyze_transport_trend import analyze as analyze_trend
@@ -34,13 +35,15 @@ def analyze_case(
3435
target_gap_min: float,
3536
target_gap_max: float,
3637
prefer_carrier: str | None,
38+
temperature_k: float,
3739
) -> dict[str, object]:
3840
band_path = locate_required(root, ["bands.dat", "band/bands.dat"])
3941
dos_path = locate_required(root, ["dos.dat", "dos/dos.dat"])
4042
mass_path = locate_optional(root, ["effective_mass.dat", "effective_mass/effective_mass.dat"])
4143
carrier = analyze_carrier(band_path, occupied_bands, fermi)
42-
trend = analyze_trend(band_path, dos_path, occupied_bands, fermi, mass_path)
44+
trend = analyze_trend(band_path, dos_path, occupied_bands, fermi, mass_path, temperature_k)
4345
mass = analyze_mass(mass_path) if mass_path is not None else None
46+
bipolar = analyze_bipolar(band_path, occupied_bands, fermi, temperature_k)
4447

4548
gap = float(carrier["band_gap_eV"])
4649
if gap < target_gap_min:
@@ -54,7 +57,8 @@ def analyze_case(
5457
mass_value = abs(float(mass["effective_mass_me"])) if mass and mass["effective_mass_me"] is not None else None
5558
mass_penalty = 0.25 * mass_value if mass_value is not None else 0.5
5659
quality_penalty = max(0.0, 0.3 - float(trend["thermoelectric_quality_score"]))
57-
score = gap_penalty + regime_penalty + carrier_penalty + mass_penalty + quality_penalty
60+
bipolar_penalty = 10.0 * float(bipolar["bipolar_risk_score"])
61+
score = gap_penalty + regime_penalty + carrier_penalty + mass_penalty + quality_penalty + bipolar_penalty
5862

5963
return {
6064
"case": root.name,
@@ -67,11 +71,14 @@ def analyze_case(
6771
"activation_energy_eV": trend["activation_energy_eV"],
6872
"thermoelectric_quality_score": trend["thermoelectric_quality_score"],
6973
"screening_class": trend["screening_class"],
74+
"bipolar_risk_score": bipolar["bipolar_risk_score"],
75+
"bipolar_risk_class": bipolar["bipolar_risk_class"],
7076
"gap_penalty_eV": gap_penalty,
7177
"regime_penalty": regime_penalty,
7278
"carrier_penalty": carrier_penalty,
7379
"mass_penalty": mass_penalty,
7480
"quality_penalty": quality_penalty,
81+
"bipolar_penalty": bipolar_penalty,
7582
"screening_score": score,
7683
}
7784

@@ -83,16 +90,18 @@ def analyze_cases(
8390
target_gap_min: float,
8491
target_gap_max: float,
8592
prefer_carrier: str | None,
93+
temperature_k: float,
8694
) -> dict[str, object]:
8795
cases = [
88-
analyze_case(root, occupied_bands, fermi, target_gap_min, target_gap_max, prefer_carrier)
96+
analyze_case(root, occupied_bands, fermi, target_gap_min, target_gap_max, prefer_carrier, temperature_k)
8997
for root in roots
9098
]
9199
ranked = sorted(cases, key=lambda item: item["screening_score"])
92100
return {
93101
"target_gap_window_eV": [target_gap_min, target_gap_max],
94102
"preferred_carrier": prefer_carrier,
95-
"ranking_basis": "screening_score = gap_penalty + regime_penalty + carrier_penalty + mass_penalty + quality_penalty",
103+
"temperature_K": temperature_k,
104+
"ranking_basis": "screening_score = gap_penalty + regime_penalty + carrier_penalty + mass_penalty + quality_penalty + bipolar_penalty",
96105
"cases": ranked,
97106
"best_case": ranked[0]["case"] if ranked else None,
98107
"observations": [
@@ -109,6 +118,7 @@ def main() -> None:
109118
parser.add_argument("--target-gap-min", type=float, default=0.5)
110119
parser.add_argument("--target-gap-max", type=float, default=1.5)
111120
parser.add_argument("--prefer-carrier", choices=["electron-like", "hole-like"])
121+
parser.add_argument("--temperature-k", type=float, default=300.0)
112122
parser.add_argument("--json", action="store_true")
113123
args = parser.parse_args()
114124
payload = analyze_cases(
@@ -118,6 +128,7 @@ def main() -> None:
118128
args.target_gap_min,
119129
args.target_gap_max,
120130
args.prefer_carrier,
131+
args.temperature_k,
121132
)
122133
if args.json:
123134
print(json.dumps(payload, indent=2))

scripts/export_transport_report.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
def screening_note(carrier: dict[str, object], mass: dict[str, object] | None, trend: dict[str, object]) -> str:
1414
if trend["regime"] == "metallic-like":
1515
return "The sampled DOS indicates a metallic-like regime, so this case is better treated as a metal or degenerate system than as a simple semiconductor."
16+
if trend["bipolar_risk_class"] == "high-bipolar-risk":
17+
return "The band gap is small enough that bipolar conduction may become a serious screening concern at the chosen temperature."
1618
if trend["screening_class"] == "promising-semiconductor-like":
1719
return f"This case looks promising in compact screening: `{carrier['carrier_tendency']}` tendency, `{trend['dopability_hint']}`, and a reasonable transport quality score."
1820
mass_value = mass["effective_mass_me"] if mass and mass["effective_mass_me"] is not None else None
@@ -38,6 +40,8 @@ def render_markdown(carrier: dict[str, object], mass: dict[str, object] | None,
3840
f"- Screening class: `{trend['screening_class']}`",
3941
f"- Transport quality score: `{trend['thermoelectric_quality_score']:.4f}`",
4042
f"- Dopability hint: `{trend['dopability_hint']}`",
43+
f"- Bipolar risk class: `{trend['bipolar_risk_class']}`",
44+
f"- Bipolar risk score: `{trend['bipolar_risk_score']:.4e}`",
4145
]
4246
if mass is not None:
4347
lines.extend(
@@ -60,13 +64,21 @@ def main() -> None:
6064
parser.add_argument("--mass-path")
6165
parser.add_argument("--occupied-bands", type=int, default=2)
6266
parser.add_argument("--fermi", type=float, required=True)
67+
parser.add_argument("--temperature-k", type=float, default=300.0)
6368
parser.add_argument("--output")
6469
args = parser.parse_args()
6570
band_path = Path(args.band_path).expanduser().resolve()
6671
dos_path = Path(args.dos_path).expanduser().resolve()
6772
carrier = analyze_carrier(band_path, args.occupied_bands, args.fermi)
6873
mass = analyze_mass(Path(args.mass_path).expanduser().resolve()) if args.mass_path else None
69-
trend = analyze_trend(band_path, dos_path, args.occupied_bands, args.fermi, Path(args.mass_path).expanduser().resolve() if args.mass_path else None)
74+
trend = analyze_trend(
75+
band_path,
76+
dos_path,
77+
args.occupied_bands,
78+
args.fermi,
79+
Path(args.mass_path).expanduser().resolve() if args.mass_path else None,
80+
args.temperature_k,
81+
)
7082
output = Path(args.output).expanduser().resolve() if args.output else Path.cwd() / "TRANSPORT_REPORT.md"
7183
output.write_text(render_markdown(carrier, mass, trend))
7284
print(output)

0 commit comments

Comments
 (0)