PFCSchemas 0.1.1__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.
- pfcschemas-0.1.1/.github/workflows/publish_pypi.yml +52 -0
- pfcschemas-0.1.1/.gitignore +10 -0
- pfcschemas-0.1.1/.python-version +1 -0
- pfcschemas-0.1.1/.vscode/settings.json +7 -0
- pfcschemas-0.1.1/PKG-INFO +14 -0
- pfcschemas-0.1.1/README.md +3 -0
- pfcschemas-0.1.1/pyproject.toml +19 -0
- pfcschemas-0.1.1/src/pfcschemas/__init__.py +2 -0
- pfcschemas-0.1.1/src/pfcschemas/ajson.py +53 -0
- pfcschemas-0.1.1/src/pfcschemas/fcj.py +196 -0
- pfcschemas-0.1.1/src/pfcschemas/ma.py +64 -0
- pfcschemas-0.1.1/src/pfcschemas/maninfo.py +53 -0
- pfcschemas-0.1.1/src/pfcschemas/positioning.py +100 -0
- pfcschemas-0.1.1/src/pfcschemas/py.typed +0 -0
- pfcschemas-0.1.1/src/pfcschemas/sdef.py +31 -0
- pfcschemas-0.1.1/src/pfcschemas/sinfo.py +113 -0
- pfcschemas-0.1.1/src/pfcschemas/utils/__init__.py +0 -0
- pfcschemas-0.1.1/src/pfcschemas/utils/enum.py +31 -0
- pfcschemas-0.1.1/src/pfcschemas/utils/files.py +13 -0
- pfcschemas-0.1.1/tests/data/fc_json.json +1 -0
- pfcschemas-0.1.1/tests/data/flight.ajson +234067 -0
- pfcschemas-0.1.1/tests/test_ajson.py +8 -0
- pfcschemas-0.1.1/tests/test_fcj.py +8 -0
- pfcschemas-0.1.1/tests/test_position.py +23 -0
- pfcschemas-0.1.1/uv.lock +289 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Publish to PyPI and create GH release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v[0-9]+.[0-9]+.[0-9]+'
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: "Set up Python"
|
|
16
|
+
uses: actions/setup-python@v4
|
|
17
|
+
with:
|
|
18
|
+
python-version-file: "pyproject.toml"
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: pip install .
|
|
22
|
+
|
|
23
|
+
- name: Install pypa / build
|
|
24
|
+
run: python3 -m pip install build --user
|
|
25
|
+
|
|
26
|
+
- name: Build a binary wheel and a source tarball
|
|
27
|
+
run: python3 -m build
|
|
28
|
+
|
|
29
|
+
- name: Store the distribution packages
|
|
30
|
+
uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: python-package-distributions
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
publish-to-pypi:
|
|
36
|
+
name: Publish Python distribution to PyPI
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment:
|
|
40
|
+
name: pypi
|
|
41
|
+
url: https://pypi.org/p/PFCSchemas
|
|
42
|
+
permissions:
|
|
43
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
44
|
+
|
|
45
|
+
steps:
|
|
46
|
+
- name: Download all the dists
|
|
47
|
+
uses: actions/download-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: python-package-distributions
|
|
50
|
+
path: dist/
|
|
51
|
+
- name: Publish distribution to PyPI
|
|
52
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: PFCSchemas
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Pydantic schemas for the pyflightcoach libraries
|
|
5
|
+
Author-email: Thomas David <thomasdavid0@gmail.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: numpy>=2.1.3
|
|
8
|
+
Requires-Dist: pandas>=2.2.3
|
|
9
|
+
Requires-Dist: pydantic>=2.9.2
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# PFCSchemas
|
|
13
|
+
|
|
14
|
+
Pydantic schemas for the pyflightcoach libraries
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "PFCSchemas"
|
|
3
|
+
description = "Pydantic schemas for the pyflightcoach libraries"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
authors = [{ name = "Thomas David", email = "thomasdavid0@gmail.com" }]
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = ["numpy>=2.1.3", "pandas>=2.2.3", "pydantic>=2.9.2"]
|
|
8
|
+
dynamic = ["version"]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
12
|
+
build-backend = "hatchling.build"
|
|
13
|
+
|
|
14
|
+
[tool.hatch.version]
|
|
15
|
+
source = "vcs"
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = ["pytest>=8.3.3"]
|
|
19
|
+
lint = ["ruff>=0.7.3"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from pfcschemas import fcj
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pfcschemas.utils.files import validate_json
|
|
7
|
+
from pfcschemas.ma import MA
|
|
8
|
+
from pfcschemas.sinfo import ScheduleInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AJson(BaseModel):
|
|
12
|
+
origin: fcj.Origin | None = None
|
|
13
|
+
isComp: bool
|
|
14
|
+
sourceBin: str | None = None
|
|
15
|
+
sourceFCJ: str | None = None
|
|
16
|
+
bootTime: datetime | None = None
|
|
17
|
+
mans: list[MA]
|
|
18
|
+
|
|
19
|
+
def schedule(self):
|
|
20
|
+
schedules = [man.schedule for man in self.mans]
|
|
21
|
+
if all([s == schedules[0] for s in schedules[1:]]):
|
|
22
|
+
return schedules[0].fcj_to_pfc()
|
|
23
|
+
else:
|
|
24
|
+
return ScheduleInfo.mixed()
|
|
25
|
+
|
|
26
|
+
def all_versions(self):
|
|
27
|
+
versions = set()
|
|
28
|
+
for man in self.mans:
|
|
29
|
+
versions |= set(man.history.keys())
|
|
30
|
+
return list(versions)
|
|
31
|
+
|
|
32
|
+
def get_scores(self, version: str, props: fcj.ScoreProperties=None, group="total"):
|
|
33
|
+
if props is None:
|
|
34
|
+
props = fcj.ScoreProperties()
|
|
35
|
+
scores = {}
|
|
36
|
+
for man in self.mans:
|
|
37
|
+
if version in man.history:
|
|
38
|
+
scores[man.name] = man.history[version].get_score(props).__dict__[group]
|
|
39
|
+
return pd.Series(scores, name=version)
|
|
40
|
+
|
|
41
|
+
def create_score_df(
|
|
42
|
+
self, props: fcj.ScoreProperties, group="total", version: str = "All"
|
|
43
|
+
):
|
|
44
|
+
versions = self.all_versions() if version == "All" else [version]
|
|
45
|
+
return pd.concat([self.get_scores(ver, props, group) for ver in versions], axis=1)
|
|
46
|
+
|
|
47
|
+
def check_version(self, version: str):
|
|
48
|
+
version = version[1:] if version.startswith('v') else version
|
|
49
|
+
return all([man.history is not None and version in man.history.keys() for man in self.mans])
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def parse_json(json: dict|str):
|
|
53
|
+
return AJson.model_validate(validate_json(json))
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FCJ(BaseModel):
|
|
12
|
+
version: str
|
|
13
|
+
comments: str
|
|
14
|
+
name: str
|
|
15
|
+
view: View
|
|
16
|
+
parameters: Parameters
|
|
17
|
+
scored: bool
|
|
18
|
+
scores: list[float]
|
|
19
|
+
human_scores: list[HumanResult] = []
|
|
20
|
+
fcs_scores: list[Result] = []
|
|
21
|
+
mans: list[Man]
|
|
22
|
+
data: list[Data]
|
|
23
|
+
jhash: int | None = None
|
|
24
|
+
|
|
25
|
+
def score_df(self):
|
|
26
|
+
return pd.concat(
|
|
27
|
+
{fcjr.fa_version: fcjr.to_df() for fcjr in self.fcs_scores},
|
|
28
|
+
axis=0,
|
|
29
|
+
names=["version", "manoeuvre", "difficulty", "truncate"],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def man_df(self):
|
|
33
|
+
return pd.DataFrame(
|
|
34
|
+
[man.__dict__ for man in self.mans[1:-1]],
|
|
35
|
+
index=pd.Index(range(len(self.mans[1:-1])), name="manoeuvre"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def pfc_version_df(self):
|
|
39
|
+
sdf = self.score_df().loc[pd.IndexSlice[:, :, 3, False]]
|
|
40
|
+
return pd.concat(
|
|
41
|
+
[sdf, sdf.mul(self.man_df().k, axis=0)], axis=1, keys=["raw", "kfac"]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def version_summary_df(self):
|
|
45
|
+
return self.pfc_version_df().groupby("version").kfac.sum()
|
|
46
|
+
|
|
47
|
+
def latest_version(self):
|
|
48
|
+
return max([fcjr.fa_version for fcjr in self.fcs_scores])
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def id(self):
|
|
52
|
+
return re.search(r"\d{8}", self.name)[0]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def created(self):
|
|
56
|
+
try:
|
|
57
|
+
return datetime.datetime.strptime(
|
|
58
|
+
re.search(r"_\d{2}_\d{2}_\d{2}_", self.name)[0], "_%y_%m_%d_"
|
|
59
|
+
)
|
|
60
|
+
except Exception:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class View(BaseModel):
|
|
65
|
+
position: dict
|
|
66
|
+
target: dict
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Parameters(BaseModel):
|
|
70
|
+
rotation: float
|
|
71
|
+
start: int
|
|
72
|
+
stop: int
|
|
73
|
+
moveEast: float
|
|
74
|
+
moveNorth: float
|
|
75
|
+
wingspan: float
|
|
76
|
+
modelwingspan: float
|
|
77
|
+
elevate: float
|
|
78
|
+
originLat: float
|
|
79
|
+
originLng: float
|
|
80
|
+
originAlt: float
|
|
81
|
+
pilotLat: str | float
|
|
82
|
+
pilotLng: str | float
|
|
83
|
+
pilotAlt: str | float
|
|
84
|
+
centerLat: str | float
|
|
85
|
+
centerLng: str | float
|
|
86
|
+
centerAlt: str | float
|
|
87
|
+
schedule: list[str]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class HumanResult(BaseModel):
|
|
91
|
+
name: str
|
|
92
|
+
date: datetime.date
|
|
93
|
+
scores: list[float]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Result(BaseModel):
|
|
97
|
+
fa_version: str
|
|
98
|
+
manresults: list[ManResult | None]
|
|
99
|
+
|
|
100
|
+
def to_df(self) -> pd.DataFrame:
|
|
101
|
+
return pd.concat(
|
|
102
|
+
{i: fcjmr.to_df() for i, fcjmr in enumerate(self.manresults[1:]) if fcjmr},
|
|
103
|
+
axis=0,
|
|
104
|
+
names=["manoeuvre", "difficulty", "truncate"],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ManResult(BaseModel):
|
|
109
|
+
els: list[El]
|
|
110
|
+
results: list[Score] = []
|
|
111
|
+
|
|
112
|
+
def to_df(self) -> pd.DataFrame:
|
|
113
|
+
return pd.DataFrame(
|
|
114
|
+
data=[res.score.__dict__ for res in self.results],
|
|
115
|
+
index=pd.MultiIndex.from_frame(
|
|
116
|
+
pd.DataFrame([res.properties.__dict__ for res in self.results])
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def get_score(self, props: ScoreProperties):
|
|
121
|
+
for r in self.results:
|
|
122
|
+
if r.properties == props:
|
|
123
|
+
return r.score
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class El(BaseModel):
|
|
127
|
+
name: str
|
|
128
|
+
start: int
|
|
129
|
+
stop: int
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Score(BaseModel):
|
|
133
|
+
score: ScoreValues
|
|
134
|
+
properties: ScoreProperties
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ScoreValues(BaseModel):
|
|
138
|
+
intra: float
|
|
139
|
+
inter: float
|
|
140
|
+
positioning: float
|
|
141
|
+
total: float
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ScoreProperties(BaseModel):
|
|
145
|
+
difficulty: int = 3
|
|
146
|
+
truncate: bool = False
|
|
147
|
+
|
|
148
|
+
def __eq__(self, other):
|
|
149
|
+
if not isinstance(other, ScoreProperties):
|
|
150
|
+
return False
|
|
151
|
+
return self.difficulty == other.difficulty and self.truncate == other.truncate
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Man(BaseModel):
|
|
155
|
+
name: str
|
|
156
|
+
k: float
|
|
157
|
+
id: str
|
|
158
|
+
sp: int
|
|
159
|
+
wd: float
|
|
160
|
+
start: int
|
|
161
|
+
stop: int
|
|
162
|
+
sel: bool
|
|
163
|
+
background: str
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class Data(BaseModel):
|
|
167
|
+
VN: float = None
|
|
168
|
+
VE: float = None
|
|
169
|
+
VD: float = None
|
|
170
|
+
dPD: float = None #
|
|
171
|
+
r: float
|
|
172
|
+
p: float
|
|
173
|
+
yw: float
|
|
174
|
+
N: float
|
|
175
|
+
E: float
|
|
176
|
+
D: float
|
|
177
|
+
time: int
|
|
178
|
+
roll: float
|
|
179
|
+
pitch: float
|
|
180
|
+
yaw: float
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_scores(file: str) -> pd.DataFrame:
|
|
184
|
+
fcj = FCJ.model_validate_json(open(file, "r").read())
|
|
185
|
+
return fcj.pfc_version_df()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Origin(BaseModel):
|
|
189
|
+
lat: Annotated[float, "latitude in degrees"]
|
|
190
|
+
lng: Annotated[float, "latitude in degrees"]
|
|
191
|
+
alt: Annotated[float, "height AMSL in meters"]
|
|
192
|
+
heading: Annotated[float, "heading (direction of box Y axis) in degrees from North"]
|
|
193
|
+
move_north: Annotated[float, "non-standard offset of box"]=0
|
|
194
|
+
move_east: Annotated[float, "non-standard offset of box"]=0
|
|
195
|
+
move_down: Annotated[float, "non-standard offset of box"]=0
|
|
196
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from pfcschemas import fcj
|
|
7
|
+
from pfcschemas.sinfo import ScheduleInfo
|
|
8
|
+
from pfcschemas.maninfo import ManInfo
|
|
9
|
+
|
|
10
|
+
class MDef(BaseModel):
|
|
11
|
+
info: ManInfo
|
|
12
|
+
mps: dict
|
|
13
|
+
eds: dict
|
|
14
|
+
box: dict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MA(BaseModel):
|
|
18
|
+
name: str
|
|
19
|
+
id: int
|
|
20
|
+
schedule: ScheduleInfo
|
|
21
|
+
schedule_direction: str | None
|
|
22
|
+
flown: list[dict] | dict
|
|
23
|
+
|
|
24
|
+
history: dict[str, fcj.ManResult] | None = None
|
|
25
|
+
|
|
26
|
+
mdef: MDef | None = None
|
|
27
|
+
manoeuvre: dict | None = None
|
|
28
|
+
template: list[dict] | dict | None = None
|
|
29
|
+
corrected: dict | None = None
|
|
30
|
+
corrected_template: list[dict] | dict | None = None
|
|
31
|
+
scores: dict | None = None
|
|
32
|
+
|
|
33
|
+
def basic(self):
|
|
34
|
+
return MA(
|
|
35
|
+
name=self.name,
|
|
36
|
+
id=self.id,
|
|
37
|
+
schedule=self.schedule,
|
|
38
|
+
schedule_direction=self.schedule_direction,
|
|
39
|
+
flown=self.flown,
|
|
40
|
+
history=self.history,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def add_mdef(self, mdef: MDef) -> MA:
|
|
44
|
+
return MA(**(self.__dict__ | dict(mdef=mdef)))
|
|
45
|
+
|
|
46
|
+
def simplify_history(self):
|
|
47
|
+
vnames = [v[1:] if v.startswith("v") else v for v in self.history.keys()]
|
|
48
|
+
vnames_old = vnames[::-1]
|
|
49
|
+
vnids = [
|
|
50
|
+
len(vnames) - vnames_old.index(vn) - 1
|
|
51
|
+
for vn in list(pd.Series(vnames).unique())
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
return MA(
|
|
55
|
+
**(
|
|
56
|
+
self.__dict__
|
|
57
|
+
| dict(
|
|
58
|
+
history={vnames[i]: list(self.history.values())[i] for i in vnids}
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# vids = [vnames.rindex(vn) for vn in set(vnames)]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from .positioning import Position, BoxLocation
|
|
2
|
+
from typing import Tuple, Annotated
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ManInfo(BaseModel):
|
|
7
|
+
name: str
|
|
8
|
+
short_name: str
|
|
9
|
+
k: float
|
|
10
|
+
position: Position
|
|
11
|
+
start: BoxLocation
|
|
12
|
+
end: BoxLocation
|
|
13
|
+
centre_points: Annotated[
|
|
14
|
+
list[int],
|
|
15
|
+
"points that should be centered, ids correspond to the previous element",
|
|
16
|
+
] = []
|
|
17
|
+
centred_els: Annotated[
|
|
18
|
+
list[Tuple[int, float]], "element ids that should be centered"
|
|
19
|
+
] = []
|
|
20
|
+
|
|
21
|
+
def to_dict(self):
|
|
22
|
+
return self.model_dump()
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_dict(data: dict):
|
|
26
|
+
return ManInfo.model_validate(data)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def maninfomaker(
|
|
30
|
+
name: str,
|
|
31
|
+
short_name: str,
|
|
32
|
+
k: float,
|
|
33
|
+
position: Position,
|
|
34
|
+
start: BoxLocation,
|
|
35
|
+
end: BoxLocation,
|
|
36
|
+
centre_points: Annotated[
|
|
37
|
+
list[int],
|
|
38
|
+
"points that should be centered, ids correspond to the previous element",
|
|
39
|
+
] = None,
|
|
40
|
+
centred_els: Annotated[
|
|
41
|
+
list[Tuple[int, float]], "element ids that should be centered"
|
|
42
|
+
] = None,
|
|
43
|
+
):
|
|
44
|
+
return ManInfo(
|
|
45
|
+
name=name,
|
|
46
|
+
short_name=short_name,
|
|
47
|
+
k=k,
|
|
48
|
+
position=position,
|
|
49
|
+
start=start,
|
|
50
|
+
end=end,
|
|
51
|
+
centre_points=centre_points if centre_points is not None else [],
|
|
52
|
+
centred_els=centred_els if centred_els is not None else [],
|
|
53
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
from .utils.enum import EnumStr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Orientation(EnumStr):
|
|
8
|
+
UPRIGHT = np.pi
|
|
9
|
+
INVERTED = 0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Heading(EnumStr):
|
|
13
|
+
RTOL = np.pi
|
|
14
|
+
LTOR = 0
|
|
15
|
+
OUTTOIN = 3 * np.pi / 2
|
|
16
|
+
INTOOUT = np.pi / 2
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def values():
|
|
20
|
+
return np.array(list(Heading.__members__.values()))
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def infer(bearing: Annotated[float, "in radians from north"]):
|
|
24
|
+
def check(bearing: float, heading: Heading):
|
|
25
|
+
return (
|
|
26
|
+
np.round(np.abs(4 * (bearing - heading.value)) / (2 * np.pi)).astype(
|
|
27
|
+
int
|
|
28
|
+
)
|
|
29
|
+
% 4
|
|
30
|
+
) == 0
|
|
31
|
+
|
|
32
|
+
for head in Heading.__members__.values():
|
|
33
|
+
if check(bearing, head):
|
|
34
|
+
return head
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f"Invalid bearing {bearing}")
|
|
37
|
+
|
|
38
|
+
def reverse(self):
|
|
39
|
+
return {
|
|
40
|
+
Heading.RTOL: Heading.LTOR,
|
|
41
|
+
Heading.LTOR: Heading.RTOL,
|
|
42
|
+
Heading.OUTTOIN: Heading.INTOOUT,
|
|
43
|
+
Heading.INTOOUT: Heading.OUTTOIN,
|
|
44
|
+
}[self]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Direction(EnumStr):
|
|
48
|
+
UPWIND = 1
|
|
49
|
+
DOWNWIND = -1
|
|
50
|
+
CROSS = 0
|
|
51
|
+
|
|
52
|
+
def wind_swap_heading(self, d_or_w: Heading) -> int:
|
|
53
|
+
match self:
|
|
54
|
+
case Direction.UPWIND:
|
|
55
|
+
return d_or_w
|
|
56
|
+
case Direction.DOWNWIND:
|
|
57
|
+
return d_or_w.reverse()
|
|
58
|
+
case Direction.CROSS:
|
|
59
|
+
return d_or_w
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def parse(s: str):
|
|
63
|
+
match s[0].lower():
|
|
64
|
+
case "u":
|
|
65
|
+
return Direction.UPWIND
|
|
66
|
+
case "d":
|
|
67
|
+
return Direction.DOWNWIND
|
|
68
|
+
case "c":
|
|
69
|
+
return Direction.CROSS
|
|
70
|
+
case _:
|
|
71
|
+
raise ValueError(f"Invalid wind {s}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Height(EnumStr):
|
|
75
|
+
BTM = 0.2
|
|
76
|
+
MID = 0.6
|
|
77
|
+
TOP = 1.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Position(EnumStr):
|
|
81
|
+
CENTRE = 0
|
|
82
|
+
END = 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class BoxLocation(BaseModel):
|
|
86
|
+
height: Height
|
|
87
|
+
direction: Direction | None = None
|
|
88
|
+
orientation: Orientation | None = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def boxlocationmaker(
|
|
92
|
+
height: Height,
|
|
93
|
+
direction: Direction | None = None,
|
|
94
|
+
orientation: Orientation | None = None,
|
|
95
|
+
):
|
|
96
|
+
return BoxLocation(
|
|
97
|
+
height=height,
|
|
98
|
+
direction=direction,
|
|
99
|
+
orientation=orientation,
|
|
100
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from flightanalysis import SchedDef
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from pfcschemas.sinfo import ScheduleInfo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DirectionDefinition(BaseModel):
|
|
8
|
+
manid: int
|
|
9
|
+
direction: str
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def from_sdef(sdef: SchedDef):
|
|
13
|
+
manid = sdef.wind_def_manoeuvre()
|
|
14
|
+
return DirectionDefinition(
|
|
15
|
+
manid=manid, direction=sdef[manid].info.start.direction.name
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SDefFile(BaseModel):
|
|
20
|
+
category: str
|
|
21
|
+
schedule: str
|
|
22
|
+
direction_definition: DirectionDefinition
|
|
23
|
+
fa_version: str
|
|
24
|
+
mdefs: dict[str, dict | list[dict]]
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def sinfo(self):
|
|
28
|
+
return ScheduleInfo(self.category, self.schedule)
|
|
29
|
+
|
|
30
|
+
def create_definition(self):
|
|
31
|
+
return SchedDef.from_dict(self.mdefs)
|