- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
84 lines
2.2 KiB
Python
84 lines
2.2 KiB
Python
class CefrMapper:
|
|
"""Maps IRT theta values to CEFR levels and IELTS band scores."""
|
|
|
|
THETA_TO_CEFR = [
|
|
(-4.0, -2.5, 'pre_a1'),
|
|
(-2.5, -1.5, 'a1'),
|
|
(-1.5, -0.5, 'a2'),
|
|
(-0.5, 0.5, 'b1'),
|
|
(0.5, 1.5, 'b2'),
|
|
(1.5, 2.5, 'c1'),
|
|
(2.5, 4.0, 'c2'),
|
|
]
|
|
|
|
CEFR_TO_BAND = {
|
|
'pre_a1': 2.0,
|
|
'a1': 3.0,
|
|
'a2': 4.0,
|
|
'b1': 5.0,
|
|
'b2': 6.5,
|
|
'c1': 7.5,
|
|
'c2': 9.0,
|
|
}
|
|
|
|
CEFR_LABELS = {
|
|
'pre_a1': 'Pre-A1 (Beginner)',
|
|
'a1': 'A1 (Elementary)',
|
|
'a2': 'A2 (Pre-Intermediate)',
|
|
'b1': 'B1 (Intermediate)',
|
|
'b2': 'B2 (Upper-Intermediate)',
|
|
'c1': 'C1 (Advanced)',
|
|
'c2': 'C2 (Proficient)',
|
|
}
|
|
|
|
@staticmethod
|
|
def theta_to_cefr(theta):
|
|
for low, high, level in CefrMapper.THETA_TO_CEFR:
|
|
if low <= theta < high:
|
|
return level
|
|
return 'c2' if theta >= 2.5 else 'pre_a1'
|
|
|
|
@staticmethod
|
|
def theta_to_band(theta):
|
|
cefr = CefrMapper.theta_to_cefr(theta)
|
|
base_band = CefrMapper.CEFR_TO_BAND.get(cefr, 5.0)
|
|
|
|
for low, high, level in CefrMapper.THETA_TO_CEFR:
|
|
if level == cefr:
|
|
range_width = high - low
|
|
if range_width > 0:
|
|
position = (theta - low) / range_width
|
|
else:
|
|
position = 0.5
|
|
|
|
cefr_list = list(CefrMapper.CEFR_TO_BAND.keys())
|
|
idx = cefr_list.index(cefr)
|
|
next_band = CefrMapper.CEFR_TO_BAND.get(
|
|
cefr_list[min(idx + 1, len(cefr_list) - 1)], base_band + 1.0
|
|
)
|
|
|
|
band = base_band + position * (next_band - base_band)
|
|
return round(band * 2) / 2
|
|
|
|
return base_band
|
|
|
|
@staticmethod
|
|
def band_to_cefr(band):
|
|
if band < 2.5:
|
|
return 'pre_a1'
|
|
if band < 3.5:
|
|
return 'a1'
|
|
if band < 4.5:
|
|
return 'a2'
|
|
if band < 5.5:
|
|
return 'b1'
|
|
if band < 7.0:
|
|
return 'b2'
|
|
if band < 8.0:
|
|
return 'c1'
|
|
return 'c2'
|
|
|
|
@staticmethod
|
|
def get_cefr_label(cefr_code):
|
|
return CefrMapper.CEFR_LABELS.get(cefr_code, cefr_code)
|