From b4950cee355449c48659349bcdfc99bcdd2dcc72 Mon Sep 17 00:00:00 2001 From: AlanSilvaaa Date: Tue, 26 May 2026 12:52:01 -0400 Subject: [PATCH] feat: add join-corresponding-sums --- ENDPOINTS-EXAMPLE.md | 15 +++ app/problems/grade_1/__init__.py | 2 + .../join-corresponding-sums.png | Bin 0 -> 32742 bytes ...ty.png => join-pictures-with-quantity.png} | Bin .../grade_1/join_corresponding_sums.py | 96 ++++++++++++++++++ app/routers/grade_1.py | 23 +++++ app/schemas/grade_1/__init__.py | 6 ++ .../grade_1/join_corresponding_sums.py | 32 ++++++ .../test_join_corresponding_sums_endpoint.py | 64 ++++++++++++ 9 files changed, 238 insertions(+) create mode 100644 app/problems/grade_1/images_for_reference/join-corresponding-sums.png rename app/problems/grade_1/images_for_reference/{join_pictures_with_quantity.png => join-pictures-with-quantity.png} (100%) create mode 100644 app/problems/grade_1/join_corresponding_sums.py create mode 100644 app/schemas/grade_1/join_corresponding_sums.py create mode 100644 tests/test_join_corresponding_sums_endpoint.py diff --git a/ENDPOINTS-EXAMPLE.md b/ENDPOINTS-EXAMPLE.md index e4bea12..2f84c6a 100644 --- a/ENDPOINTS-EXAMPLE.md +++ b/ENDPOINTS-EXAMPLE.md @@ -4,6 +4,21 @@ Test quick examples for each endpoint: ## Grade 1 +### Join corresponding sums + +```bash +curl -X POST "http://127.0.0.1:8000/math/grade_1/join_corresponding_sums" \ + -H "Content-Type: application/json" \ + -d '{ + "pair_count": 3, + "min_sum": 2, + "max_sum": 10, + "min_addend": 1, + "max_addend": 9, + "seed": 1 + }' +``` + ### Join pictures with quantity ```bash diff --git a/app/problems/grade_1/__init__.py b/app/problems/grade_1/__init__.py index fe0c1c8..a4d8adb 100644 --- a/app/problems/grade_1/__init__.py +++ b/app/problems/grade_1/__init__.py @@ -1,6 +1,7 @@ from app.problems.grade_1.compose_and_decompose_numbers import ( compose_and_decompose_numbers, ) +from app.problems.grade_1.join_corresponding_sums import join_corresponding_sums from app.problems.grade_1.join_pictures_with_quantity import ( join_pictures_with_quantity, ) @@ -9,6 +10,7 @@ from app.problems.grade_1.where_are_more_items import where_are_more_items __all__ = [ "compose_and_decompose_numbers", + "join_corresponding_sums", "join_pictures_with_quantity", "sum_with_image_reference", "where_are_more_items", diff --git a/app/problems/grade_1/images_for_reference/join-corresponding-sums.png b/app/problems/grade_1/images_for_reference/join-corresponding-sums.png new file mode 100644 index 0000000000000000000000000000000000000000..3553fa6830c5688765f6afb092f82a617b50b52f GIT binary patch literal 32742 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�rrDG6UMu4_T>#mn=f-;VDi*B!iw=7R^=N3Cmfng z3eO*BJ6tAkOyTG9JM!z*ThC<0%-L906(N%Ciq~3&KaGqGGBs& z21v_`|DCPu4aLXn>FLI$K0GnMT%qnI2n6L~ zEGhhu;!6T0p2N3Reegm@-Ro!5^YSOTyXTKLrLi@;kQG!IVizY?#IF_S?q|!YlxaWG zG1owSYtPt{>2AuDl`eM|3X-3Wj)|U=qWtagrDIegrP#PNrpDd6H!Nm&7@K-lBJ5;_ z&HIc=e8pbdKh*nMK5z_w?wuj!^GVhBDP#52@07D`6zi>&dOe=p-HER_VSmSeSY4^>-BT}3_3N71Cc)#LNQ`2XucaU*2( zD?h}^kRw&7^kVY}x^cox5f@z+e4x{`-;!BmY+x+^B}SFc;KFMo$JTIUbnr){O_giQ zPg4nwgRYJ8I?j%mJH9?KvF7@w=r0&_`TDJ1JIaG%oT#X{>SM{vww#ju@DzV$7yr3y z05PK5W7)8DVGtX;=&@o%D|E3TViCCFbQ%yK<-I{Cn-vnU7!+;-- z`N^?IMhdE1PQ%qPDR`!+WKRQt373;|@v3)rZA_pn1s>N%f!$dY1EbtfQmrMSnHIsodk_w!+1MJ5W4A597nT>_nNSD#9U< zjW0V3+I|JkL$>`7we?&R19|A)+hX_Kp- z7$AzL1=71#%h^@#QyiLqT^;5D;$&%rE8FVGxukq+bh4I!ww$liknBA%$o7T<;NV9z zZPbQBe>+w1AzL673)h343cE#{Drb>@%c(Zpwt#~ruR(u+X8r!|CH%{NYNqo@V z-*u7{gnyJmmtVOxty(acm`LPGXYVFDwMz`kAmBpg9mvO=Q~xhDvbC& zz}xlbs1Q8q{o*~9kb_iKWG{rU6%OD6)@g<;)wr~@AI@)1`?KxD=z%1%&`WE%bg-J0 zx4J|&1Dp3B>|)PR{qPVw>(Ra%KMu6le1HE?O=6o_zI|41mtUwE35+qZnO^6B;r5Qo zJU4d>6^c=|SNDz%e1oILlPUZ{Me8+xilf_)ys9!vjGcwWrlWJ4z)8aq%8~-$@fkbBHRlx6m23fFtnbkz- zEpX+_GkuB^P7typq$O*~hyByDYHQ8jlY$^AE{vJaye2=#jJoW9^()Gt_&Xx!nu1MR zW6(cbo0*vza$a<(YIozH7}1-;C+C--Q?qd;`M2|T=HU=>`|rtv$eL18$^`_er#G30E1-g&i}a|98JxXn z;8O_AcMZMXXqH^}leeiF{Fu+#M$W%FyoNM<4P#H=OZ$>0%IaAUE|+7@K|>=topIfm zj;#07Gpf6{K9(v7-RY$Zgnt(>#j)KDWxK7IRwZWYM&1WvPt;LEX*ACft_wZL^%c_Axkgi`J()z2=V#<@Ck-rbghua0 zg>i~cJJiu-@fMxheHrk}IMn!8AURw#v5?!7upSlcD6)*yZ)SQjCI0S7q?t(@&Q4A3 ze&HJ%AC3+wiL|eLA{{v5R`y6`iII-l9L|mg?a~eJzaenYUG@hNK}wMzbn(J;AszxRRaP<1pH}s?DK9Un1p9im} zo7#_bmp8k(_$0lX*{h|U1?}kT-;2=ilVK4>+IK8T-#bxl*NJFFVRkK>3A!vzQi`+V z1Ajo5yB9DZxO8+c287;6eA0^qh7k@74!m2&3rom!PnI;k!W1M*r9!b>$?9Im(-7(D zk(Y<>6TJD< zL}q1?>V~6R_R*!H+EzPqsopX$d3w^~NhVXPp?*7XWHf5Dw$VDbPXMkE9V0Ub(=SdF zT_xcMr~N-`V;4Wa%##W{rz?@e-yns=l}Ioa6x)waRP~tt7|CDd1&%iK#YX&|UvUoz z_`tZ`oT0IHYHowGunQPFe;c8xO7jGS^hl=XV$TxU~bZEPs$ zuGJ)6M0? zxvpr15YmUVe|_p)iWtG}oe(xBm%OD9U1ndraf!|+%x{lZAJ8+Hnw0TJy(4e@&%r@I z7_^tlzkDea^6`%87B}R?kUzc8<8s6JsyZDW$lZXQaL8G(&O0V)v%hzpjE-rW-EOf> zHnxX=ZE&f%=KT-Z#sUe?A<>eYhUYsiSL@ z^Qm(fxbrh+Pg_I&QBvb`Z#=z?e;4dE&XgbL`wnMgLUU>L*-ENsaKst9Q)p>>;abxS zlFv`5Ek-XS7yfO!BoW;ANkoHZwjC2cQQ6XwAnfkl3fmzi9u^lb#mCB?EYUVSKl}4> zSYoLRw#MwK5s!CHSdGGdVK}m#7a)DNb9sEjX7H@t? z99oSSpIlP}7r)7De(O%aH!sDKvVe~cu~LUK*TMwNzUuMci+usKq>$m<8e^U^O580> z|Kt4D`tB_fC)552Yr12>kq98ha$hSPRF!z6E~q?w2nv*qdz21wthPUzvt2kt?b1Hj zikJWs7218V6@EjUD*`64@%9q%may3owun(clyq#mo|^?qCpHl_QeHi^+{v_rI4ui2 zD$r_FPp3L8AdFFL=?Ya$9?rj}L=3|6yjiHKYLeJk(w5z%dL}qAMKr!JtB@}{DQ>gw%UhIiw|RHbH*F#B zRFvc;S}>)a8eX`;HfM9JNb4@Fj4)^sB@5t>ELQ9D>;{~R*T$0_wrL;a8G62`0|ti! zebe0+`ZoX}T)#Bc%Oldth)zG=%aG0cgA?8GDa;7*Pq`j1L|T+|nK`^@@$O;3C+Nd< zu0+Mg9^)7K51tHvGVOUL_!{tiianXRmw|4>Twar{H(*MqUBi3(P&(cRPfdh|1og|i zl3V~|>R!yS7LY377137A_M3{_4?TvIu1p^~Nl;iK%k>b7+`7iAD>F!cTSSQNyO8Bq zkR>

ZtuCFAAF5J|C`ElAJFKIv4h3|1MqH>*?5$ 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 gwEwH?fD9pC65M~hPkYcy1bn dict: + """Generate addition expressions that students connect by equal sums.""" + if pair_count < 1: + raise ValueError("pair_count must be at least 1") + if min_sum > max_sum: + raise ValueError("min_sum must be less than or equal to max_sum") + if min_addend > max_addend: + raise ValueError("min_addend must be less than or equal to max_addend") + + expressions_by_total: dict[int, list[tuple[int, int]]] = {} + for total in range(min_sum, max_sum + 1): + expressions = [] + for first_addend in range(min_addend, max_addend + 1): + second_addend = total - first_addend + if min_addend <= second_addend <= max_addend: + expressions.append((first_addend, second_addend)) + + if len(expressions) >= 2: + expressions_by_total[total] = expressions + + available_totals = list(expressions_by_total) + if len(available_totals) < pair_count: + raise ValueError("sum and addend ranges must contain enough matchable sums") + + rng = random.Random(seed) + selected_totals = rng.sample(available_totals, pair_count) + left_items: list[dict] = [] + right_items: list[dict] = [] + + for index, total in enumerate(selected_totals, start=1): + left_expression, right_expression = rng.sample(expressions_by_total[total], 2) + match_id = f"sum-{total}" + left_items.append( + { + "first_addend": left_expression[0], + "second_addend": left_expression[1], + "total": total, + "match_id": match_id, + } + ) + right_items.append( + { + "first_addend": right_expression[0], + "second_addend": right_expression[1], + "total": total, + "match_id": match_id, + } + ) + + rng.shuffle(left_items) + rng.shuffle(right_items) + + left_expressions = [ + AdditionExpression(position=index + 1, **item) + for index, item in enumerate(left_items) + ] + right_expressions = [ + AdditionExpression(position=index + 1, **item) + for index, item in enumerate(right_items) + ] + right_positions_by_match_id = { + expression.match_id: expression.position for expression in right_expressions + } + answer_key = [ + SumConnection( + match_id=expression.match_id, + total=expression.total, + left_position=expression.position, + right_position=right_positions_by_match_id[expression.match_id], + ) + for expression in left_expressions + ] + + problem = JoinCorrespondingSumsProblem( + left_expressions=left_expressions, + right_expressions=right_expressions, + answer_key=answer_key, + ) + + return problem.model_dump() diff --git a/app/routers/grade_1.py b/app/routers/grade_1.py index 34ad5ae..6ef1ab2 100644 --- a/app/routers/grade_1.py +++ b/app/routers/grade_1.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, HTTPException from app.problems.grade_1 import ( compose_and_decompose_numbers, + join_corresponding_sums, join_pictures_with_quantity, sum_with_image_reference, where_are_more_items, @@ -9,6 +10,8 @@ from app.problems.grade_1 import ( from app.schemas.grade_1 import ( ComposeAndDecomposeNumbersProblem, ComposeAndDecomposeNumbersRequest, + JoinCorrespondingSumsProblem, + JoinCorrespondingSumsRequest, JoinPicturesWithQuantityProblem, JoinPicturesWithQuantityRequest, SumWithImageReferenceProblem, @@ -20,6 +23,26 @@ from app.schemas.grade_1 import ( router = APIRouter(prefix="/grade_1", tags=["Grade 1"]) +@router.post( + "/join_corresponding_sums", + response_model=JoinCorrespondingSumsProblem, +) +def create_join_corresponding_sums_problem( + request: JoinCorrespondingSumsRequest, +) -> dict: + try: + return join_corresponding_sums( + pair_count=request.pair_count, + min_sum=request.min_sum, + max_sum=request.max_sum, + min_addend=request.min_addend, + max_addend=request.max_addend, + seed=request.seed, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @router.post( "/compose_and_decompose_numbers", response_model=ComposeAndDecomposeNumbersProblem, diff --git a/app/schemas/grade_1/__init__.py b/app/schemas/grade_1/__init__.py index d6b89cd..1cfba8a 100644 --- a/app/schemas/grade_1/__init__.py +++ b/app/schemas/grade_1/__init__.py @@ -2,6 +2,10 @@ from app.schemas.grade_1.compose_and_decompose_numbers import ( ComposeAndDecomposeNumbersProblem, ComposeAndDecomposeNumbersRequest, ) +from app.schemas.grade_1.join_corresponding_sums import ( + JoinCorrespondingSumsProblem, + JoinCorrespondingSumsRequest, +) from app.schemas.grade_1.join_pictures_with_quantity import ( JoinPicturesWithQuantityProblem, JoinPicturesWithQuantityRequest, @@ -18,6 +22,8 @@ from app.schemas.grade_1.where_are_more_items import ( __all__ = [ "ComposeAndDecomposeNumbersProblem", "ComposeAndDecomposeNumbersRequest", + "JoinCorrespondingSumsProblem", + "JoinCorrespondingSumsRequest", "JoinPicturesWithQuantityProblem", "JoinPicturesWithQuantityRequest", "SumWithImageReferenceProblem", diff --git a/app/schemas/grade_1/join_corresponding_sums.py b/app/schemas/grade_1/join_corresponding_sums.py new file mode 100644 index 0000000..9d4d07d --- /dev/null +++ b/app/schemas/grade_1/join_corresponding_sums.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel, Field, PositiveInt + + +class AdditionExpression(BaseModel): + position: PositiveInt + first_addend: PositiveInt + second_addend: PositiveInt + total: PositiveInt + match_id: str = Field(min_length=1) + + +class SumConnection(BaseModel): + match_id: str = Field(min_length=1) + total: PositiveInt + left_position: PositiveInt + right_position: PositiveInt + + +class JoinCorrespondingSumsProblem(BaseModel): + instructions: str = "Conecta." + left_expressions: list[AdditionExpression] + right_expressions: list[AdditionExpression] + answer_key: list[SumConnection] + + +class JoinCorrespondingSumsRequest(BaseModel): + pair_count: PositiveInt = 3 + min_sum: PositiveInt = 2 + max_sum: PositiveInt = 10 + min_addend: PositiveInt = 1 + max_addend: PositiveInt = 9 + seed: int | None = None diff --git a/tests/test_join_corresponding_sums_endpoint.py b/tests/test_join_corresponding_sums_endpoint.py new file mode 100644 index 0000000..f194504 --- /dev/null +++ b/tests/test_join_corresponding_sums_endpoint.py @@ -0,0 +1,64 @@ +import unittest + +from fastapi.testclient import TestClient + +from app.main import create_app + + +class JoinCorrespondingSumsEndpointTest(unittest.TestCase): + def setUp(self) -> None: + self.client = TestClient(create_app()) + + def test_creates_problem_with_matching_sums(self) -> None: + response = self.client.post( + "/math/grade_1/join_corresponding_sums", + json={"pair_count": 3, "seed": 1}, + ) + + self.assertEqual(response.status_code, 200) + problem = response.json() + + self.assertEqual(problem["instructions"], "Conecta.") + self.assertEqual(len(problem["left_expressions"]), 3) + self.assertEqual(len(problem["right_expressions"]), 3) + self.assertEqual(len(problem["answer_key"]), 3) + + left_by_position = { + expression["position"]: expression + for expression in problem["left_expressions"] + } + right_by_position = { + expression["position"]: expression + for expression in problem["right_expressions"] + } + + for expression in problem["left_expressions"] + problem["right_expressions"]: + self.assertEqual( + expression["first_addend"] + expression["second_addend"], + expression["total"], + ) + + for connection in problem["answer_key"]: + left_expression = left_by_position[connection["left_position"]] + right_expression = right_by_position[connection["right_position"]] + + self.assertEqual(left_expression["total"], right_expression["total"]) + self.assertEqual(left_expression["total"], connection["total"]) + self.assertEqual(left_expression["match_id"], right_expression["match_id"]) + self.assertEqual(left_expression["match_id"], connection["match_id"]) + + def test_returns_bad_request_for_impossible_ranges(self) -> None: + response = self.client.post( + "/math/grade_1/join_corresponding_sums", + json={"pair_count": 3, "min_sum": 2, "max_sum": 2}, + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json(), + {"detail": "sum and addend ranges must contain enough matchable sums"}, + ) + + +if __name__ == "__main__": + unittest.main()