flightanalysis 0.3.20__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/PKG-INFO +3 -3
  2. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/pyproject.toml +3 -3
  3. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/__init__.py +1 -1
  4. flightanalysis-0.4.0/src/flightanalysis/analysis/__init__.py +3 -0
  5. flightanalysis-0.4.0/src/flightanalysis/analysis/aligment_optimisation.py +171 -0
  6. flightanalysis-0.4.0/src/flightanalysis/analysis/el_analysis.py +338 -0
  7. flightanalysis-0.4.0/src/flightanalysis/analysis/manoeuvre_analysis.py +340 -0
  8. flightanalysis-0.3.20/src/flightanalysis/analysis/sch_analysis.py → flightanalysis-0.4.0/src/flightanalysis/analysis/schedule_analysis.py +14 -47
  9. flightanalysis-0.4.0/src/flightanalysis/base/ref_funcs.py +154 -0
  10. flightanalysis-0.4.0/src/flightanalysis/base/utils.py +107 -0
  11. flightanalysis-0.4.0/src/flightanalysis/builders/__init__.py +3 -0
  12. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/builders/elbuilders.py +88 -24
  13. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/builders/manbuilder.py +88 -25
  14. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/collectors.py +35 -25
  15. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/eldef.py +18 -14
  16. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/mandef.py +35 -22
  17. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/manoption.py +4 -1
  18. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/manparm.py +51 -38
  19. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/operations/funopp.py +2 -1
  20. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/operations/operation.py +1 -1
  21. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/scheddef.py +2 -2
  22. flightanalysis-0.4.0/src/flightanalysis/elements/element.py +230 -0
  23. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/elements/line.py +12 -7
  24. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/elements/loop.py +23 -11
  25. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/elements/snap.py +24 -7
  26. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/elements/spin.py +33 -12
  27. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/elements/stall_turn.py +16 -3
  28. flightanalysis-0.4.0/src/flightanalysis/elements/tags.py +131 -0
  29. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/elements/tail_slide.py +7 -5
  30. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/manoeuvre.py +23 -44
  31. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/__init__.py +1 -1
  32. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/box/box.py +14 -16
  33. flightanalysis-0.4.0/src/flightanalysis/scoring/box/parser.py +40 -0
  34. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/criteria/__init__.py +27 -4
  35. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/criteria/criteria.py +10 -11
  36. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/criteria/criteria_group.py +3 -0
  37. flightanalysis-0.4.0/src/flightanalysis/scoring/criteria/exponential.py +99 -0
  38. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/criteria/inter/combination.py +15 -7
  39. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/criteria/inter/comparison.py +6 -2
  40. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/criteria/intra/bounded.py +21 -5
  41. flightanalysis-0.4.0/src/flightanalysis/scoring/criteria/intra/continuous.py +156 -0
  42. flightanalysis-0.4.0/src/flightanalysis/scoring/criteria/intra/deviation.py +36 -0
  43. flightanalysis-0.4.0/src/flightanalysis/scoring/criteria/intra/peak.py +72 -0
  44. flightanalysis-0.4.0/src/flightanalysis/scoring/criteria/intra/single.py +51 -0
  45. flightanalysis-0.4.0/src/flightanalysis/scoring/criteria/parser.py +49 -0
  46. flightanalysis-0.4.0/src/flightanalysis/scoring/downgrade/__init__.py +6 -0
  47. flightanalysis-0.4.0/src/flightanalysis/scoring/downgrade/base.py +38 -0
  48. flightanalysis-0.4.0/src/flightanalysis/scoring/downgrade/downgrade.py +131 -0
  49. flightanalysis-0.4.0/src/flightanalysis/scoring/downgrade/downgrade_pair.py +88 -0
  50. flightanalysis-0.4.0/src/flightanalysis/scoring/downgrade/downgrades.py +58 -0
  51. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/measurement.py +32 -21
  52. flightanalysis-0.4.0/src/flightanalysis/scoring/reffuncs.py +53 -0
  53. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/results/elements_results.py +26 -1
  54. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/results/manoeuvre_results.py +19 -1
  55. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/results/result.py +67 -56
  56. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/results/results.py +71 -35
  57. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/visibility.py +2 -12
  58. flightanalysis-0.4.0/tests/data/fcs_csv.csv +3 -0
  59. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/test_schedule_element_spin.py +5 -5
  60. {flightanalysis-0.3.20/tests → flightanalysis-0.4.0/tests/test_scoring}/test_criteria.py +8 -1
  61. flightanalysis-0.4.0/tests/test_scoring/test_lookup.py +11 -0
  62. flightanalysis-0.4.0/tests/test_utils.py +15 -0
  63. flightanalysis-0.3.20/src/flightanalysis/analysis/__init__.py +0 -3
  64. flightanalysis-0.3.20/src/flightanalysis/analysis/el_analysis.py +0 -145
  65. flightanalysis-0.3.20/src/flightanalysis/analysis/manoeuvre_analysis/__init__.py +0 -14
  66. flightanalysis-0.3.20/src/flightanalysis/analysis/manoeuvre_analysis/alignment.py +0 -131
  67. flightanalysis-0.3.20/src/flightanalysis/analysis/manoeuvre_analysis/analysis.py +0 -10
  68. flightanalysis-0.3.20/src/flightanalysis/analysis/manoeuvre_analysis/basic.py +0 -219
  69. flightanalysis-0.3.20/src/flightanalysis/analysis/manoeuvre_analysis/complete.py +0 -262
  70. flightanalysis-0.3.20/src/flightanalysis/analysis/manoeuvre_analysis/scored.py +0 -50
  71. flightanalysis-0.3.20/src/flightanalysis/base/ref_funcs.py +0 -80
  72. flightanalysis-0.3.20/src/flightanalysis/base/utils.py +0 -24
  73. flightanalysis-0.3.20/src/flightanalysis/builders/example/__init__.py +0 -4
  74. flightanalysis-0.3.20/src/flightanalysis/builders/example/box.py +0 -12
  75. flightanalysis-0.3.20/src/flightanalysis/builders/example/criteria.py +0 -30
  76. flightanalysis-0.3.20/src/flightanalysis/builders/example/downgrades.py +0 -22
  77. flightanalysis-0.3.20/src/flightanalysis/builders/example/manbuilder.py +0 -116
  78. flightanalysis-0.3.20/src/flightanalysis/definition/dg_applicator.py +0 -18
  79. flightanalysis-0.3.20/src/flightanalysis/elements/element.py +0 -121
  80. flightanalysis-0.3.20/src/flightanalysis/scoring/criteria/exponential.py +0 -57
  81. flightanalysis-0.3.20/src/flightanalysis/scoring/criteria/intra/continuous.py +0 -74
  82. flightanalysis-0.3.20/src/flightanalysis/scoring/criteria/intra/deviation.py +0 -15
  83. flightanalysis-0.3.20/src/flightanalysis/scoring/criteria/intra/peak.py +0 -38
  84. flightanalysis-0.3.20/src/flightanalysis/scoring/criteria/intra/single.py +0 -37
  85. flightanalysis-0.3.20/src/flightanalysis/scoring/downgrade.py +0 -154
  86. flightanalysis-0.3.20/src/flightanalysis/scoring/reffuncs.py +0 -39
  87. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/.github/workflows/publish_pypi.yml +0 -0
  88. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/.gitignore +0 -0
  89. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/LICENSE +0 -0
  90. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/README.md +0 -0
  91. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/examples/__init__.py +0 -0
  92. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/examples/criteria.ipynb +0 -0
  93. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/examples/making_elements.py +0 -0
  94. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/examples/manoeuvre_definition.py +0 -0
  95. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/examples/reversing_spin.py +0 -0
  96. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/examples/tailslide.py +0 -0
  97. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/__init__.py +0 -0
  98. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/operations/__init__.py +0 -0
  99. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/operations/itemopp.py +0 -0
  100. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/operations/mathopp.py +0 -0
  101. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/definition/operations/sumopp.py +0 -0
  102. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/elements/__init__.py +0 -0
  103. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/py.typed +0 -0
  104. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/schedule.py +0 -0
  105. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/box/__init__.py +0 -0
  106. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/box/rectangular_box.py +0 -0
  107. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/box/triangular_box.py +0 -0
  108. {flightanalysis-0.3.20/src/flightanalysis/builders → flightanalysis-0.4.0/src/flightanalysis/scoring/criteria/inter}/__init__.py +0 -0
  109. {flightanalysis-0.3.20/src/flightanalysis/scoring/criteria/inter → flightanalysis-0.4.0/src/flightanalysis/scoring/criteria/intra}/__init__.py +0 -0
  110. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/results/__init__.py +0 -0
  111. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/results/dgplot.py +0 -0
  112. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/results/summary.py +0 -0
  113. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/scoring/results.py +0 -0
  114. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/src/flightanalysis/version.py +0 -0
  115. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/EmailedBox.f3a +0 -0
  116. {flightanalysis-0.3.20/src/flightanalysis/scoring/criteria/intra → flightanalysis-0.4.0/tests}/__init__.py +0 -0
  117. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/conftest.py +0 -0
  118. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/data/manual_F3A_P23.json +0 -0
  119. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/data/old_json.json +0 -0
  120. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/data/p23.BIN +0 -0
  121. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/data/p23_box.f3a +0 -0
  122. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/data/p23_fc.json +0 -0
  123. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/data/p23_flight.json +0 -0
  124. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/data/scored_fcj.json +0 -0
  125. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/data/unscored_fcj.json +0 -0
  126. {flightanalysis-0.3.20/tests → flightanalysis-0.4.0/tests/test_definition}/__init__.py +0 -0
  127. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_operations.py +0 -0
  128. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_rfuncs.py +0 -0
  129. {flightanalysis-0.3.20/tests/test_definition → flightanalysis-0.4.0/tests/test_schedule}/__init__.py +0 -0
  130. {flightanalysis-0.3.20/tests/test_schedule → flightanalysis-0.4.0/tests/test_schedule/test_element}/__init__.py +0 -0
  131. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/loop_analysis.json +0 -0
  132. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/p23_th_e0.csv +0 -0
  133. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/p23_th_e0.json +0 -0
  134. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/p23_th_e0_template.csv +0 -0
  135. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/test_schedule_element.py +0 -0
  136. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/test_schedule_element_line.py +0 -0
  137. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/test_schedule_element_loop.py +0 -0
  138. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/test_schedule_element_snap.py +0 -0
  139. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_element/test_schedule_element_stallturn.py +0 -0
  140. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/tests/test_schedule/test_manoeuvre.py +0 -0
  141. {flightanalysis-0.3.20/tests/test_schedule/test_element → flightanalysis-0.4.0/tests/test_scoring}/__init__.py +0 -0
  142. {flightanalysis-0.3.20/tests → flightanalysis-0.4.0/tests/test_scoring}/test_box.py +0 -0
  143. {flightanalysis-0.3.20 → flightanalysis-0.4.0}/uv.lock +0 -0
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flightanalysis
3
- Version: 0.3.20
3
+ Version: 0.4.0
4
4
  Summary: Python tools for Analysis of Flight Data
5
5
  Author-email: Thomas David <thomasdavid0@gmail.com>
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.12
8
8
  Requires-Dist: fastdtw>=0.3.4
9
- Requires-Dist: flightdata>=0.3.4
9
+ Requires-Dist: flightdata>=0.3.5
10
10
  Requires-Dist: joblib>=1.4.2
11
11
  Requires-Dist: json-stream>=2.3.2
12
12
  Requires-Dist: loguru>=0.7.2
13
+ Requires-Dist: more-itertools>=10.8.0
13
14
  Requires-Dist: numpy>=2.1.3
14
15
  Requires-Dist: pandas>=2.2.3
15
- Requires-Dist: pfcschemas>=0.1.12
16
16
  Requires-Dist: scipy>=1.14.1
17
17
  Requires-Dist: simplejson>=3.19.3
18
18
  Provides-Extra: dataflash
@@ -1,19 +1,19 @@
1
1
  [project]
2
2
  name = "flightanalysis"
3
- version="0.3.20"
3
+ version="0.4.0"
4
4
  description = "Python tools for Analysis of Flight Data"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Thomas David", email = "thomasdavid0@gmail.com" }]
7
7
  requires-python = ">=3.12"
8
8
  dependencies = [
9
9
  "fastdtw>=0.3.4",
10
- "flightdata>=0.3.4",
10
+ "flightdata>=0.3.5",
11
11
  "joblib>=1.4.2",
12
12
  "json-stream>=2.3.2",
13
13
  "loguru>=0.7.2",
14
+ "more-itertools>=10.8.0",
14
15
  "numpy>=2.1.3",
15
16
  "pandas>=2.2.3",
16
- "pfcschemas>=0.1.12",
17
17
  "scipy>=1.14.1",
18
18
  "simplejson>=3.19.3",
19
19
  ]
@@ -4,7 +4,7 @@ from .manoeuvre import Manoeuvre # noqa: F401
4
4
  from .schedule import Schedule # noqa: F401
5
5
  from .definition import * # noqa: F403
6
6
  from .scoring import * # noqa: F403
7
- from .analysis import ScheduleAnalysis, ElementAnalysis, manoeuvre_analysis as ma # noqa: F401
7
+ from .analysis import ScheduleAnalysis, ElementAnalysis, Analysis # noqa: F401
8
8
 
9
9
  import sys
10
10
  from loguru import logger
@@ -0,0 +1,3 @@
1
+ from .el_analysis import ElementAnalysis
2
+ from .manoeuvre_analysis import Analysis
3
+ from .schedule_analysis import ScheduleAnalysis
@@ -0,0 +1,171 @@
1
+ import numpy as np
2
+ from flightanalysis.scoring.results import Results
3
+ from flightanalysis.definition import ElDef, ManDef
4
+ from flightanalysis.elements import AnyElement, Elements
5
+ from flightanalysis.manoeuvre import Manoeuvre
6
+ from flightdata import State
7
+ import geometry as g
8
+ from loguru import logger
9
+
10
+
11
+ def get_score(
12
+ ed: ElDef, el: AnyElement, itrans: g.Transformation, fl: State
13
+ ) -> tuple[Results, g.Transformation]:
14
+ el: AnyElement = el.match_intention(itrans, fl)
15
+ tp = el.create_template(State.from_transform(itrans), fl)
16
+ return ed.dgs.apply(el, fl, tp), el, tp # tp[-1].att
17
+
18
+
19
+ def _optimise_split(
20
+ mdef: ManDef,
21
+ manoeuvre: Manoeuvre,
22
+ templates: dict[str, State],
23
+ eln1: str,
24
+ eln2: str,
25
+ itrans: g.Transformation,
26
+ fl: State,
27
+ min_len: int = 3,
28
+ step_size: int = 1,
29
+ include_inter: bool = True,
30
+ ) -> int:
31
+ def score_split(steps: int) -> float:
32
+ new_fl = fl.step_label("element", eln1, steps, fl.t, min_len)
33
+ res1, oel1, tp1 = get_score(
34
+ mdef.eds[eln1], manoeuvre.elements[eln1], itrans, getattr(new_fl.element, eln1)
35
+ )
36
+
37
+ el2fl = getattr(new_fl.element, eln2)
38
+ res2, oel2, tp2 = get_score(
39
+ mdef.eds[eln2], manoeuvre.elements[eln2], g.Transformation(tp1[-1].att, el2fl[0].pos), el2fl
40
+ )
41
+
42
+ if include_inter:
43
+ oman = Manoeuvre.from_all_elements(
44
+ manoeuvre.uid,
45
+ Elements(manoeuvre.elements.data | {oel1.uid: oel1, oel2.uid: oel2}),
46
+ )
47
+
48
+ omdef = mdef.update_defaults(oman)
49
+
50
+ inter = omdef.mps.collect(
51
+ oman,
52
+ State.stack(templates | {oel1.uid: tp1, oel2.uid: tp2}, "element"),
53
+ mdef.box,
54
+ )
55
+
56
+ logger.debug(f"split {steps} {res1.total + res2.total:.2f}")
57
+ logger.debug(
58
+ f"e1={oel1.uid}, e2={oel2.uid}, steps={steps}, intra={res1.total + res2.total:.2f}, inter={inter.total if include_inter else 'not included'}"
59
+ )
60
+ return res1.total + res2.total + (inter.total if include_inter else 0)
61
+
62
+ dgs = {0: score_split(0)}
63
+
64
+ def check_steps(stps: int):
65
+ new_l2 = len(getattr(fl.element, eln2)) - stps + 1
66
+ new_l1 = len(getattr(fl.element, eln1)) + stps + 1
67
+ return new_l2 > min_len and new_l1 > min_len
68
+
69
+ steps = step_size * (int(len(getattr(fl.element, eln1)) > len(getattr(fl.element, eln2))) * 2 - 1)
70
+
71
+ if not check_steps(steps):
72
+ return 0
73
+
74
+ try:
75
+ new_dg = score_split(steps)
76
+ if new_dg > dgs[0]:
77
+ steps = -steps
78
+ else:
79
+ dgs[steps] = new_dg
80
+ steps += np.sign(steps) * step_size
81
+ except Exception:
82
+ steps = -steps
83
+
84
+ while check_steps(steps):
85
+ try:
86
+ new_dg = score_split(steps)
87
+ if new_dg < list(dgs.values())[-1]:
88
+ dgs[steps] = new_dg
89
+ steps += np.sign(steps) * step_size
90
+ else:
91
+ break
92
+ except ValueError:
93
+ break
94
+
95
+ min_dg_step = np.argmin(np.array(list(dgs.values())))
96
+ out_steps = list(dgs.keys())[min_dg_step]
97
+ return out_steps
98
+
99
+
100
+ def optimise_alignment_old(
101
+ flown: State, mdef: ManDef, manoeuvre: Manoeuvre, templates: dict[str, State]
102
+ ) -> State:
103
+ elns = list(mdef.eds.data.keys())
104
+
105
+ padjusted = set(elns)
106
+ count = 0
107
+ while len(padjusted) > 0 and count < 2:
108
+ adjusted = set()
109
+ for eln1, eln2 in zip(elns[:-1], elns[1:]):
110
+ if (eln1 in padjusted) or (eln2 in padjusted):
111
+ itrans = g.Transformation(
112
+ templates[eln1][0].att,
113
+ flown.element[eln1][0].pos,
114
+ )
115
+ steps = _optimise_split(
116
+ mdef,
117
+ manoeuvre,
118
+ templates,
119
+ eln1, # flown.element[eln1][0],
120
+ eln2, # flown.element[eln2][0],
121
+ itrans,
122
+ flown,
123
+ )
124
+
125
+ if not steps == 0:
126
+ logger.debug(
127
+ f"Adjusting split between {eln1} and {eln2} by {steps} steps"
128
+ )
129
+ flown = flown.step_label("element", eln1, steps, flown.t, 3)
130
+ adjusted.update([eln1, eln2])
131
+
132
+ padjusted = adjusted
133
+ count += 1
134
+ logger.debug(f"pass {count}, {len(padjusted)} elements adjusted:\n{padjusted}")
135
+
136
+ return flown
137
+
138
+ def optimise_alignment(
139
+ flown: State, mdef: ManDef, manoeuvre: Manoeuvre, templates: dict[str, State]
140
+ ) -> State:
141
+
142
+ elns = list(mdef.eds.data.keys())
143
+
144
+
145
+
146
+ for eln1, eln2 in zip(elns[:-1], elns[1:]):
147
+ for step_size in [5, 1]:
148
+ itrans = g.Transformation(
149
+ templates[eln1][0].att,
150
+ flown.element[eln1][0].pos,
151
+ )
152
+ steps = _optimise_split(
153
+ mdef,
154
+ manoeuvre,
155
+ templates,
156
+ eln1, # flown.element[eln1][0],
157
+ eln2, # flown.element[eln2][0],
158
+ itrans,
159
+ flown,
160
+ 3,
161
+ step_size,
162
+ True
163
+ )
164
+
165
+ if not steps == 0:
166
+ logger.debug(
167
+ f"Adjusting split between {eln1} and {eln2} by {steps} steps"
168
+ )
169
+ flown = flown.step_label("element", eln1, steps, flown.t, 3)
170
+
171
+ return flown
@@ -0,0 +1,338 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+
4
+ from flightanalysis.scoring.results import Results, Result
5
+ from flightdata import State
6
+ from typing import Self
7
+ from flightanalysis import ElDef, Element, ManParms
8
+ from dataclasses import dataclass
9
+ import geometry as g
10
+
11
+
12
+ @dataclass
13
+ class ElementAnalysis:
14
+ edef: ElDef
15
+ mps: ManParms
16
+ el: Element
17
+ fl: State
18
+ tp: State
19
+ ref_frame: g.Transformation
20
+ results: Results | None = None
21
+
22
+ def update(self, new_fl: State):
23
+ new_el = self.el.match_intention(self.ref_frame, new_fl)
24
+ new_tp = new_el.create_template(self.tp[0], new_fl.time)
25
+ return ElementAnalysis(
26
+ self.edef, self.mps, new_el, new_fl, new_tp, self.ref_frame
27
+ )
28
+
29
+ def plot_3d(self, **kwargs):
30
+ from plotting import plotsec
31
+
32
+ return plotsec(dict(fl=self.fl, tp=self.tp), **kwargs)
33
+
34
+ def to_dict(self):
35
+ return {k: v.to_dict() for k, v in self.__dict__.items()}
36
+
37
+ @staticmethod
38
+ def from_dict(data) -> Self:
39
+ mps = ManParms.from_dict(data["mps"])
40
+ return ElementAnalysis(
41
+ ElDef.from_dict(data["edef"], mps),
42
+ mps,
43
+ Element.from_dict(data["el"]),
44
+ State.from_dict(data["fl"]),
45
+ State.from_dict(data["tp"]),
46
+ g.Transformation.from_dict(data["ref_frame"]),
47
+ )
48
+
49
+ def score_dg(self, dg: str, limits: bool = True) -> Result:
50
+ return self.edef.dgs[dg](self.el, self.fl, self.tp, limits)
51
+
52
+ def intra_score(self) -> Results:
53
+ try:
54
+ return self.edef.dgs.apply(
55
+ self.el, self.fl, self.tp
56
+ )
57
+ except Exception as e:
58
+ raise Exception(f"{self.el.uid}: {e}") from e
59
+
60
+ def info(self):
61
+ return dict(
62
+ element=self.el.uid,
63
+ **(dict(angle=np.degrees(self.el.angle)) if "angle" in self.el.parameters else {}),
64
+ **(dict(radius=self.el.radius) if "radius" in self.el.parameters else {}),
65
+ **(dict(rolls=np.degrees(self.el.rolls)) if "rolls" in self.el.parameters else {}),
66
+ **(dict(roll=np.degrees(self.el.roll)) if "roll" in self.el.parameters else {}),
67
+ freq=f"{1 / self.fl.dt.mean():.0f}",
68
+ len=len(self.fl),
69
+ **(dict(intra=f"{self.results.total:.4f}") if self.results is not None else {}),
70
+ ) # fmt: skip
71
+
72
+ def full_result_plot(self, name: str, showlegend=True, textposition: str = "bottom left"):
73
+ """plot the actual result and the uncropped result on the same figure,
74
+ include some info from the ddowngrade"""
75
+ import plotly.graph_objects as go
76
+
77
+ dg = self.edef.dgs[name]
78
+ full_res: Result = dg(self.el, self.fl, self.tp, True, select=False)
79
+ crop_res: Result = dg(self.el, self.fl, self.tp, True, select=True)
80
+ full_xvals = self.fl.t - self.fl.t[0]
81
+ crop_xvals = np.array(g.utils.get_value(full_xvals, crop_res.sample_keys))
82
+
83
+ u = full_res.plot_f
84
+
85
+ fig = go.Figure(
86
+ layout=dict(
87
+ yaxis2=dict(
88
+ title="Visibility", overlaying="y", range=[0, 1], side="right"
89
+ ),
90
+ title=f"{crop_res.name}, {crop_res.total:.2f}",
91
+ legend=dict(orientation="h", x=0, y=1.4, yanchor="top"),
92
+ margin=dict(t=10, r=90, b=90, l=90),
93
+ xaxis=dict(
94
+ visible=True,
95
+ title="Time (s)",
96
+ range=[
97
+ 0,
98
+ full_xvals[-1]
99
+ if full_xvals is not None
100
+ else len(self.measurement),
101
+ ],
102
+ ),
103
+ yaxis=dict(
104
+ title=f"Measurement ({crop_res.measurement.unit.replace('rad', 'degrees')})"
105
+ ),
106
+ ),
107
+ )
108
+ if len(crop_res.measurement) < len(full_res.measurement):
109
+ fig.add_trace(
110
+ go.Scatter(
111
+ x=full_xvals,
112
+ y=u(full_res.measurement.value),
113
+ name="Measurement",
114
+ mode="lines",
115
+ line=dict(color="blue", width=1, dash="dash"),
116
+ showlegend=showlegend
117
+ )
118
+ )
119
+
120
+ fig.add_trace(
121
+ go.Scatter(
122
+ x=crop_xvals,
123
+ y=u(crop_res.measurement.value),
124
+ name="Selected" if len(crop_res.measurement) < len(full_res.measurement) else "Measurement",
125
+ mode="lines",
126
+ line=dict(color="blue", width=1, dash="solid"),
127
+ showlegend=showlegend
128
+ )
129
+ )
130
+
131
+ if len(dg.smoothers) > 1:
132
+ fig.add_trace(
133
+ go.Scatter(
134
+ x=crop_xvals,
135
+ y=u(crop_res.raw_sample),
136
+ name="Raw Sample",
137
+ mode="lines",
138
+ line=dict(color="black", width=1, dash="dash"),
139
+ showlegend=showlegend
140
+ )
141
+ )
142
+ fig.add_trace(
143
+ go.Scatter(
144
+ x=crop_xvals,
145
+ y=u(crop_res.sample),
146
+ name="Smooth Sample" if len(dg.smoothers) > 1 else "Sample",
147
+ mode="lines",
148
+ line=dict(color="black", width=1, dash="solid"),
149
+ showlegend=showlegend
150
+ )
151
+ )
152
+ fig.add_trace(
153
+ go.Scatter(
154
+ x=crop_res.sample_keys[self.keys]
155
+ if crop_xvals is None
156
+ else crop_xvals[crop_res.keys],
157
+ y=u(crop_res.sample[crop_res.keys]),
158
+ text=np.round(crop_res.dgs, 3),
159
+ hovertext=[crop_res.info(i) for i in range(len(crop_res.keys))],
160
+ mode="markers",
161
+ name="Downgrades",
162
+ textposition=textposition,
163
+ yaxis="y",
164
+ marker=dict(size=10, color="black"),
165
+ showlegend=showlegend
166
+ )
167
+ )
168
+ fig.add_trace(
169
+ go.Scatter(
170
+ x=full_xvals,
171
+ y=full_res.measurement.visibility,
172
+ mode="lines",
173
+ name="Visibility",
174
+ yaxis="y2",
175
+ line=dict(width=1, color="black", dash="dot"),
176
+ showlegend=showlegend
177
+ )
178
+ )
179
+
180
+ fig.add_vrect(
181
+ x0=full_xvals[0],
182
+ x1=crop_xvals[0],
183
+ fillcolor="grey",
184
+ opacity=0.2,
185
+ line_width=0,
186
+ showlegend=False
187
+ )
188
+ fig.add_vrect(
189
+ x0=crop_xvals[-1],
190
+ x1=full_xvals[-1],
191
+ fillcolor="grey",
192
+ opacity=0.2,
193
+ line_width=0,
194
+ showlegend=False
195
+ )
196
+ if crop_res.criteria.__class__.__name__ == "Bounded":
197
+ fig.add_shape(
198
+ type="rect",
199
+ x0=crop_xvals[0],
200
+ x1=crop_xvals[-1],
201
+ y0=u(crop_res.criteria.max_bound) if crop_res.criteria.max_bound is not None else -50,
202
+ y1=u(crop_res.criteria.min_bound) if crop_res.criteria.min_bound is not None else 50,
203
+ fillcolor="red",
204
+ opacity=0.2,
205
+ line_width=0,
206
+ showlegend=False
207
+ )
208
+
209
+ return fig
210
+
211
+ def results_subset_plot(self, names: list[str]):
212
+ from plotly.subplots import make_subplots
213
+
214
+ figs = [self.full_result_plot(name, i==0, "bottom left") for i, name in enumerate(names)]
215
+
216
+ fig = make_subplots(
217
+ rows=len(figs),
218
+ cols=1,
219
+ shared_xaxes=True,
220
+ specs=[[{"secondary_y": True}] for _ in range(len(figs))],
221
+ vertical_spacing=0.06,
222
+ )
223
+
224
+ for i, _fig in enumerate(figs, 1):
225
+ for tr in _fig.data:
226
+ fig.add_trace(tr, row=i, col=1, secondary_y=tr.name == "Visibility")
227
+ for sh in _fig.layout.shapes:
228
+ fig.add_shape(sh, row=i, col=1)
229
+
230
+ axprops = dict(zerolinecolor="RGBA(0, 0, 0, 0.2)", zerolinewidth=2)
231
+
232
+ axes = {}
233
+ for i, _fig in enumerate(figs, 1):
234
+ axes[f"xaxis{i if i > 1 else ''}"] = dict(
235
+ visible=True, range=[0, self.fl.duration], **axprops
236
+ ) | (dict(title="Time (s)") if i == len(figs) else {})
237
+ axes[f"yaxis{2*i-1 if i>1 else ''}"] = dict(title=names[i-1], **axprops)
238
+ axes[f"yaxis{2*i}"] = dict(title="Visibility", overlaying=f"y{2*i-1 if i>1 else ''}", range=[0, 1], side="right", **axprops)
239
+
240
+ fig.update_layout(
241
+ legend=dict(orientation="h", x=0, y=1 + 0.4 / len(figs), yanchor="top"),
242
+ margin=dict(t=10, r=70, b=70, l=70),
243
+ width=1000,
244
+ height=250 * len(figs) + 50,
245
+ )
246
+
247
+
248
+ fig.update_layout(**axes)
249
+ return fig
250
+
251
+ def plot_results(self, name: str):
252
+ import plotly.graph_objects as go
253
+ from plotly.subplots import make_subplots
254
+
255
+ result: Result = self.results[name]
256
+
257
+ fig = make_subplots(
258
+ 1, 2,
259
+ column_widths=[0.25, 0.75],
260
+ specs=[[{"secondary_y": False,},{"secondary_y": True}]],
261
+ ) # fmt: skip
262
+
263
+ fig.add_trace(self.fl.pos.plotxz().data[0], row=1, col=1)
264
+
265
+ fig.add_traces(
266
+ [
267
+ *result.measurement_trace(),
268
+ *result.sample_trace(),
269
+ result.downgrade_trace(),
270
+ ],
271
+ rows=1,
272
+ cols=2,
273
+ )
274
+ fig.add_trace(
275
+ result.visibility_trace(),
276
+ row=1,
277
+ col=2,
278
+ secondary_y=True,
279
+ )
280
+ info = dict(
281
+ **self.info(),
282
+ **{"downgrade": result.total},
283
+ )
284
+
285
+ def format_float(v):
286
+ return f"{v:.2f}" if isinstance(v, float) else v
287
+
288
+ text = "<br>".join([f"{k}={format_float(v)}" for k, v in info.items()])
289
+
290
+ fig.update_layout(
291
+ yaxis=dict(scaleanchor="x", scaleratio=1),
292
+ xaxis2=dict(range=[0, len(self.fl)]),
293
+ height=300,
294
+ legend=dict(
295
+ orientation="h",
296
+ yanchor="top",
297
+ y=1.0,
298
+ xanchor="left",
299
+ x=0.3,
300
+ bgcolor="rgba(0,0,0,0)",
301
+ ),
302
+ margin=dict(l=0, r=0, t=0, b=0),
303
+ # yaxis2=dict(range=[0, 2]),
304
+ yaxis3=dict(range=[0, 1]),
305
+ annotations=[
306
+ go.layout.Annotation(
307
+ text=text,
308
+ align="left",
309
+ showarrow=False,
310
+ xref="paper",
311
+ yref="paper",
312
+ x=0.2,
313
+ y=0.8,
314
+ bordercolor="black",
315
+ borderwidth=1,
316
+ )
317
+ ],
318
+ scene=dict(
319
+ aspectmode="data",
320
+ ),
321
+ )
322
+
323
+ return fig
324
+
325
+ def axisplot(self, camera: dict = None, **kwargs):
326
+ import plotly.graph_objects as go
327
+
328
+ fig: go.Figure = self.plot_3d(**kwargs)
329
+ fig.update_layout(
330
+ scene=dict(
331
+ camera=dict(
332
+ eye=dict(x=0, y=-1, z=0),
333
+ projection=dict(type="orthographic"),
334
+ )
335
+ | (camera if camera is not None else {}),
336
+ )
337
+ )
338
+ return fig