-
Notifications
You must be signed in to change notification settings - Fork 2
Feature/sig fig #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Feature/sig fig #15
Changes from all commits
62a2d6c
4730319
1d61b31
90e68a4
a53ddc0
2f9bf1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,90 @@ | ||
| from numpy import spacing | ||
|
|
||
|
|
||
| def _is_number(value): | ||
| return isinstance(value, int) or isinstance(value, float) | ||
|
|
||
|
|
||
| def _round_to_sig_figs(value, sig_figs): | ||
| if value == 0: | ||
| return 0.0 | ||
| return float(f"{value:.{sig_figs}g}") | ||
|
|
||
|
|
||
| def _split_numeric_string(value): | ||
| if not isinstance(value, str): | ||
| return None | ||
| stripped = value.strip() | ||
| body = stripped[1:] if stripped[:1] in ("+", "-") else stripped | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will we also accept plus/minus (pm)? We have some accommodation for that concept. But perhaps it's at the preview stage which splits it into a list of multiple entries? |
||
|
|
||
| mantissa, sep, exponent = body.partition("e") if "e" in body else body.partition("E") | ||
| if sep and not exponent.lstrip("+-").isdigit(): | ||
| return None | ||
|
|
||
| has_decimal = "." in mantissa | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this used? |
||
| int_part, _, frac_part = mantissa.partition(".") | ||
| if not (int_part.isdigit() or frac_part.isdigit()): | ||
| return None | ||
| if int_part and not int_part.isdigit(): | ||
| return None | ||
| if frac_part and not frac_part.isdigit(): | ||
| return None | ||
|
|
||
| return int_part, frac_part, has_decimal | ||
|
|
||
|
|
||
| def _count_sig_figs(int_part, frac_part, has_decimal): | ||
| digits = int_part + frac_part | ||
| first_nonzero = next((i for i, d in enumerate(digits) if d != "0"), None) | ||
| if first_nonzero is None: | ||
| return 1 | ||
|
|
||
| trimmed = digits[first_nonzero:] | ||
| if has_decimal: | ||
| return len(trimmed) | ||
| return len(trimmed.rstrip("0")) or 1 | ||
|
|
||
|
|
||
| def _result(is_correct, real_diff, allowed_diff, feedback=""): | ||
| return { | ||
| "is_correct": bool(is_correct), | ||
| "real_diff": real_diff, | ||
| "allowed_diff": allowed_diff, | ||
| "feedback": feedback, | ||
| } | ||
|
|
||
|
|
||
| def _evaluate_sig_figs(response, answer, sig_figs): | ||
| parts = _split_numeric_string(response) | ||
| if parts is None: | ||
| return _result(False, None, None, "Please enter a number.") | ||
|
|
||
| response_value = float(response) | ||
| rounded_answer = _round_to_sig_figs(answer, sig_figs) | ||
| rounded_response = _round_to_sig_figs(response_value, sig_figs) | ||
| numeric_correct = abs(rounded_response - rounded_answer) <= spacing(abs(rounded_answer)) | ||
|
|
||
| precision_correct = response_value == 0 or _count_sig_figs(*parts) == sig_figs | ||
| is_correct = numeric_correct and precision_correct | ||
|
|
||
| feedback = "" | ||
| if numeric_correct and not precision_correct: | ||
| feedback = f"Please give your answer to {sig_figs} significant figures." | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Karl started collecting all feedback strings in one place, so that they can be easily maintained. Could you find that and put this string there and call it? |
||
|
|
||
| return _result(is_correct, abs(response_value - answer), None, feedback) | ||
|
|
||
|
|
||
| def _evaluate_tolerance(response, answer, relative_tolerance, absolute_tolerance): | ||
| allowed_diff = absolute_tolerance + relative_tolerance * abs(answer) + spacing(answer) | ||
|
|
||
| if not _is_number(response): | ||
| return _result(False, None, allowed_diff, "Please enter a number.") | ||
|
|
||
| real_diff = abs(response - answer) | ||
|
|
||
| return _result(real_diff <= allowed_diff, real_diff, allowed_diff) | ||
|
|
||
|
|
||
| def evaluation_function(response, answer, params) -> dict: | ||
| """ | ||
| Function used to grade a student response. | ||
|
|
@@ -21,30 +106,27 @@ def evaluation_function(response, answer, params) -> dict: | |
| to output the grading response. | ||
| """ | ||
|
|
||
| sig_figs = params.get("significant_figures", params.get("sig_figs")) | ||
| relative_tolerance = params.get("relative_tolerance", params.get("rtol", 0)) | ||
| absolute_tolerance = params.get("absolute_tolerance", params.get("atol", 0)) | ||
|
|
||
| if not (isinstance(answer, int) or isinstance(answer, float)): | ||
| raise Exception("Answer must be a number.") | ||
|
|
||
| allowed_diff = absolute_tolerance + relative_tolerance * abs(answer) | ||
| allowed_diff += spacing(answer) | ||
| uses_tolerance = any( | ||
| key in params | ||
| for key in ("relative_tolerance", "rtol", "absolute_tolerance", "atol") | ||
| ) | ||
|
|
||
| if not (isinstance(response, int) or isinstance(response, float)): | ||
| return { | ||
| "is_correct": False, | ||
| "real_diff": None, | ||
| "allowed_diff": allowed_diff, | ||
| "feedback": "Please enter a number.", | ||
| } | ||
| if sig_figs is not None and uses_tolerance: | ||
| raise Exception( | ||
| "significant_figures/sig_figs cannot be used together with " | ||
| "relative_tolerance/rtol or absolute_tolerance/atol." | ||
| ) | ||
|
|
||
| if sig_figs is not None and (not isinstance(sig_figs, int) or sig_figs < 1): | ||
| raise Exception("significant_figures must be a positive integer.") | ||
|
|
||
| real_diff = abs(response - answer) | ||
| is_correct = bool(real_diff <= allowed_diff) | ||
| if not _is_number(answer): | ||
| raise Exception("Answer must be a number.") | ||
|
|
||
| return { | ||
| "is_correct": is_correct, | ||
| "real_diff": real_diff, | ||
| "allowed_diff": allowed_diff, | ||
| "feedback": "", | ||
| } | ||
| if sig_figs is not None: | ||
| return _evaluate_sig_figs(response, answer, sig_figs) | ||
| return _evaluate_tolerance(response, answer, relative_tolerance, absolute_tolerance) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use the long form,
absolute_tolerance?