metafold 0.12.dev2__py3-none-any.whl → 0.12.dev3__py3-none-any.whl
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.
- metafold/client.py +9 -4
- metafold/simulation/__init__.py +2 -0
- metafold/simulation/compression_simulation.py +149 -21
- metafold/simulation/run_experiment.py +21 -1
- {metafold-0.12.dev2.dist-info → metafold-0.12.dev3.dist-info}/METADATA +1 -1
- {metafold-0.12.dev2.dist-info → metafold-0.12.dev3.dist-info}/RECORD +9 -9
- {metafold-0.12.dev2.dist-info → metafold-0.12.dev3.dist-info}/WHEEL +0 -0
- {metafold-0.12.dev2.dist-info → metafold-0.12.dev3.dist-info}/licenses/LICENSE +0 -0
- {metafold-0.12.dev2.dist-info → metafold-0.12.dev3.dist-info}/top_level.txt +0 -0
metafold/client.py
CHANGED
|
@@ -55,11 +55,16 @@ class Client:
|
|
|
55
55
|
headers = {"Authorization": f"Bearer {self._auth.get_token()}"}
|
|
56
56
|
r: Response = request(url, *args, **kwargs, headers=headers)
|
|
57
57
|
if not r.ok:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
# Not all error responses are JSON so fall back to the status reason
|
|
59
|
+
try:
|
|
60
|
+
body: dict[str, Any] = r.json()
|
|
61
|
+
except ValueError:
|
|
62
|
+
body = {}
|
|
61
63
|
reason = body.get("errors") or body.get("msg") or body.get("description")
|
|
62
|
-
raise HTTPError(
|
|
64
|
+
raise HTTPError(
|
|
65
|
+
f"HTTP error occurred: {reason or r.reason} "
|
|
66
|
+
f"(status {r.status_code} for {r.request.method} {r.url})"
|
|
67
|
+
)
|
|
63
68
|
return r
|
|
64
69
|
|
|
65
70
|
def get(self, url: str, *args: Any, **kwargs: Any) -> Response:
|
metafold/simulation/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from metafold.simulation.compression_simulation import (
|
|
2
|
+
BoundaryCondition,
|
|
2
3
|
CompressionSimulation,
|
|
3
4
|
ExperimentBox,
|
|
4
5
|
ExperimentCylinder,
|
|
@@ -14,6 +15,7 @@ from metafold.simulation.compression_simulation import (
|
|
|
14
15
|
ExperimentSupportCylinder,
|
|
15
16
|
ExperimentSupportMesh,
|
|
16
17
|
ExperimentSupportParallelepiped,
|
|
18
|
+
ForceSource,
|
|
17
19
|
ReferenceData,
|
|
18
20
|
SimulationParameters,
|
|
19
21
|
WorkflowStep,
|
|
@@ -21,6 +21,7 @@ from simulation_configurator import (
|
|
|
21
21
|
Contact,
|
|
22
22
|
Mpm,
|
|
23
23
|
)
|
|
24
|
+
from simulation_configurator.element import CompositeElement, Element
|
|
24
25
|
from simulation_configurator.grid import Face
|
|
25
26
|
from simulation_configurator.shapes import Box, File, Cylinder, Parallelepiped
|
|
26
27
|
from plyfile import PlyData, PlyElement
|
|
@@ -71,6 +72,68 @@ ZERO_VELOCITY = [
|
|
|
71
72
|
]
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
class BoundaryCondition(Enum):
|
|
76
|
+
SYMMETRIC = "symmetric" # symmetry plane (acts as frictionless wall)
|
|
77
|
+
VELOCITY_DIRICHLET = "velocity_dirichlet" # velocity fixed to zero at the face
|
|
78
|
+
VELOCITY_NEUMANN = "velocity_neumann" # zero velocity gradient (free face)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
DEFAULT_BOUNDARY_CONDITIONS = {
|
|
82
|
+
"x-": BoundaryCondition.SYMMETRIC,
|
|
83
|
+
"x+": BoundaryCondition.SYMMETRIC,
|
|
84
|
+
"y-": BoundaryCondition.SYMMETRIC,
|
|
85
|
+
"y+": BoundaryCondition.SYMMETRIC,
|
|
86
|
+
"z-": BoundaryCondition.VELOCITY_DIRICHLET,
|
|
87
|
+
"z+": BoundaryCondition.VELOCITY_DIRICHLET,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _velocity_neumann_face(side: str):
|
|
92
|
+
# simulation_configurator.Face only provides fixed (symmetry) and unfixed
|
|
93
|
+
# (velocity Dirichlet) faces; build the Neumann variant here until it can
|
|
94
|
+
# move into a simulation-configurator release.
|
|
95
|
+
face = CompositeElement("Face", {"side": side})
|
|
96
|
+
bc_type = CompositeElement(
|
|
97
|
+
"BCType",
|
|
98
|
+
{"id": "all", "label": "Velocity", "var": "Neumann"},
|
|
99
|
+
)
|
|
100
|
+
bc_type.add(Element("value", data="[0.0, 0.0, 0.0]"))
|
|
101
|
+
face.add(bc_type)
|
|
102
|
+
return face
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_FACE_BUILDERS = {
|
|
106
|
+
BoundaryCondition.SYMMETRIC: Face.fixed,
|
|
107
|
+
BoundaryCondition.VELOCITY_DIRICHLET: Face.unfixed,
|
|
108
|
+
BoundaryCondition.VELOCITY_NEUMANN: _velocity_neumann_face,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _normalize_boundary_conditions(
|
|
113
|
+
boundary_conditions: dict,
|
|
114
|
+
) -> dict[str, BoundaryCondition]:
|
|
115
|
+
"""Merge per-face boundary conditions over the defaults.
|
|
116
|
+
|
|
117
|
+
Values may be BoundaryCondition members or their string values (as parsed
|
|
118
|
+
from an experiment manifest). Faces not present keep their default.
|
|
119
|
+
"""
|
|
120
|
+
unknown = set(boundary_conditions) - set(DEFAULT_BOUNDARY_CONDITIONS)
|
|
121
|
+
if unknown:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"Unknown boundary condition face(s) {sorted(unknown)}; "
|
|
124
|
+
f"expected faces {sorted(DEFAULT_BOUNDARY_CONDITIONS)}"
|
|
125
|
+
)
|
|
126
|
+
resolved = dict(DEFAULT_BOUNDARY_CONDITIONS)
|
|
127
|
+
for side, bc in boundary_conditions.items():
|
|
128
|
+
resolved[side] = BoundaryCondition(bc) if isinstance(bc, str) else bc
|
|
129
|
+
return resolved
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ForceSource(Enum):
|
|
133
|
+
BOUNDARY_FORCE = "boundary_force" # uses boundary_force_zminus (default)
|
|
134
|
+
RIGID_REACTION_FORCE = "rigid_reaction_force" # uses rigid reaction force of force_source_part
|
|
135
|
+
|
|
136
|
+
|
|
74
137
|
@dataclass
|
|
75
138
|
class SimulationParameters:
|
|
76
139
|
init_time: float = 0.0
|
|
@@ -89,6 +152,15 @@ class SimulationParameters:
|
|
|
89
152
|
extra_cells: list[int] = field(default_factory=lambda: [1, 1, 1])
|
|
90
153
|
patches: list[int] = field(default_factory=lambda: [4, 2, 2])
|
|
91
154
|
|
|
155
|
+
# Per-face boundary conditions; faces omitted here use the default.
|
|
156
|
+
boundary_conditions: dict[str, BoundaryCondition] = field(
|
|
157
|
+
default_factory=lambda: dict(DEFAULT_BOUNDARY_CONDITIONS)
|
|
158
|
+
)
|
|
159
|
+
force_source: ForceSource = ForceSource.BOUNDARY_FORCE
|
|
160
|
+
# Part whose rigid_reaction_force feeds force-displacement when
|
|
161
|
+
# force_source is RIGID_REACTION_FORCE. Empty string means the piston.
|
|
162
|
+
force_source_part: str = ""
|
|
163
|
+
|
|
92
164
|
|
|
93
165
|
@dataclass
|
|
94
166
|
class ExperimentPart:
|
|
@@ -506,6 +578,21 @@ class CompressionSimulation:
|
|
|
506
578
|
p.restore_from_state_dict(saved)
|
|
507
579
|
break
|
|
508
580
|
|
|
581
|
+
@staticmethod
|
|
582
|
+
def cancel_active_workflows(client: MetafoldClient, project_id: str) -> list[str]:
|
|
583
|
+
"""Cancel all in-flight (pending/started) workflows for a project.
|
|
584
|
+
|
|
585
|
+
Returns the number of workflows cancelled. Used when re-running an
|
|
586
|
+
experiment on an existing project, whose previous results we're about
|
|
587
|
+
to overwrite anyway.
|
|
588
|
+
"""
|
|
589
|
+
cancelled = []
|
|
590
|
+
for state in ("pending", "started"):
|
|
591
|
+
for wf in client.workflows.list(q=f"state:{state}", project_id=project_id):
|
|
592
|
+
client.workflows.cancel(wf.id, project_id=project_id)
|
|
593
|
+
cancelled.append(wf.id)
|
|
594
|
+
return cancelled
|
|
595
|
+
|
|
509
596
|
@staticmethod
|
|
510
597
|
def find_or_create_project(
|
|
511
598
|
project_name: str,
|
|
@@ -514,12 +601,17 @@ class CompressionSimulation:
|
|
|
514
601
|
client_secret: Optional[str] = None,
|
|
515
602
|
auth_domain: str = "metafold3d.us.auth0.com",
|
|
516
603
|
base_url: str = "https://api.metafold3d.com/",
|
|
604
|
+
cancel_existing_workflows: bool = True,
|
|
517
605
|
) -> str:
|
|
518
606
|
"""Find an existing project by name or create a new one.
|
|
519
607
|
|
|
520
608
|
Supply either access_token (server context — never reads from env) or
|
|
521
609
|
client_id + client_secret (local/programmable use, reads from env via
|
|
522
610
|
setup_client). base_url must match the audience the token was issued for.
|
|
611
|
+
|
|
612
|
+
When cancel_existing_workflows is True and an existing project is
|
|
613
|
+
reused, its running workflows are cancelled first.
|
|
614
|
+
|
|
523
615
|
Returns the project_id string.
|
|
524
616
|
"""
|
|
525
617
|
client = MetafoldClient(
|
|
@@ -534,7 +626,10 @@ class CompressionSimulation:
|
|
|
534
626
|
except HTTPError:
|
|
535
627
|
existing = []
|
|
536
628
|
if existing:
|
|
537
|
-
|
|
629
|
+
project_id = existing[0].id
|
|
630
|
+
if cancel_existing_workflows:
|
|
631
|
+
CompressionSimulation.cancel_active_workflows(client, project_id)
|
|
632
|
+
return project_id
|
|
538
633
|
created = client.projects.create(
|
|
539
634
|
project_name,
|
|
540
635
|
access=Access.PRIVATE,
|
|
@@ -775,17 +870,28 @@ class CompressionSimulation:
|
|
|
775
870
|
|
|
776
871
|
grid_min = np.array(grid_patch["offset"], dtype=np.float32) * 1e-3
|
|
777
872
|
grid_max = np.array(grid_patch["size"], dtype=np.float32) * 1e-3 + grid_min
|
|
778
|
-
grid_min[:2] -= self.simulation_parameters.margin_xy
|
|
779
|
-
grid_max[:2] += self.simulation_parameters.margin_xy
|
|
780
|
-
grid_max[2] += self.simulation_parameters.margin_z
|
|
781
873
|
|
|
782
|
-
# Expand grid to contain every
|
|
874
|
+
# Expand grid to contain every part's bounding box, so nothing is
|
|
875
|
+
# clipped (e.g. a support/outsole extending below the representative
|
|
876
|
+
# part). Primitives report bounds via get_bounds() in metres, mesh
|
|
877
|
+
# parts via their sampled patch (offset/size in mm)
|
|
783
878
|
for info in self.part_infos:
|
|
784
879
|
part = info.part
|
|
785
880
|
if isinstance(part, ExperimentPrimitive):
|
|
786
881
|
pmin, pmax = part.get_bounds()
|
|
787
|
-
|
|
788
|
-
|
|
882
|
+
elif info.patch:
|
|
883
|
+
pmin = np.array(info.patch["offset"], dtype=np.float32) / 1000.0
|
|
884
|
+
pmax = np.array(info.patch["size"], dtype=np.float32) / 1000.0 + pmin
|
|
885
|
+
else:
|
|
886
|
+
continue
|
|
887
|
+
grid_min = np.minimum(grid_min, pmin)
|
|
888
|
+
grid_max = np.maximum(grid_max, pmax)
|
|
889
|
+
|
|
890
|
+
# Apply margins after all parts included
|
|
891
|
+
grid_min[:2] -= self.simulation_parameters.margin_xy
|
|
892
|
+
grid_max[:2] += self.simulation_parameters.margin_xy
|
|
893
|
+
grid_min[2] -= self.simulation_parameters.margin_z
|
|
894
|
+
grid_max[2] += self.simulation_parameters.margin_z
|
|
789
895
|
|
|
790
896
|
grid_size = grid_max - grid_min
|
|
791
897
|
spacing = (
|
|
@@ -807,10 +913,11 @@ class CompressionSimulation:
|
|
|
807
913
|
}
|
|
808
914
|
)
|
|
809
915
|
bcs = BoundaryConditions()
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
916
|
+
boundary_conditions = _normalize_boundary_conditions(
|
|
917
|
+
self.simulation_parameters.boundary_conditions
|
|
918
|
+
)
|
|
919
|
+
for side, bc in boundary_conditions.items():
|
|
920
|
+
bcs._sub_elements[side] = _FACE_BUILDERS[bc](side)
|
|
814
921
|
grid.add(bcs)
|
|
815
922
|
|
|
816
923
|
store = GeometryStore()
|
|
@@ -867,16 +974,18 @@ class CompressionSimulation:
|
|
|
867
974
|
store.add(geometry_element)
|
|
868
975
|
|
|
869
976
|
outputs_indices_key = ",".join(str(i) for i in range(len(self.part_infos)))
|
|
977
|
+
all_outputs = [
|
|
978
|
+
"boundary_force_zminus",
|
|
979
|
+
"boundary_force_zplus",
|
|
980
|
+
"kinetic_energy",
|
|
981
|
+
"strain_energy",
|
|
982
|
+
]
|
|
983
|
+
if self.simulation_parameters.force_source == ForceSource.RIGID_REACTION_FORCE:
|
|
984
|
+
all_outputs.append("rigid_reaction_force")
|
|
870
985
|
archive = Archive(
|
|
871
986
|
outputs={
|
|
872
987
|
outputs_indices_key: ["deformation_measure", "position", "stress"],
|
|
873
|
-
"all":
|
|
874
|
-
"boundary_force_zminus",
|
|
875
|
-
"boundary_force_zplus",
|
|
876
|
-
"rigid_reaction_force_0",
|
|
877
|
-
"kinetic_energy",
|
|
878
|
-
"strain_energy",
|
|
879
|
-
],
|
|
988
|
+
"all": all_outputs,
|
|
880
989
|
},
|
|
881
990
|
output_interval=self.simulation_parameters.output_int,
|
|
882
991
|
)
|
|
@@ -1052,6 +1161,22 @@ class CompressionSimulation:
|
|
|
1052
1161
|
)
|
|
1053
1162
|
return piston_info
|
|
1054
1163
|
|
|
1164
|
+
def _get_force_source_info(self):
|
|
1165
|
+
"""PartInfo of the body whose rigid_reaction_force is measured."""
|
|
1166
|
+
name = self.simulation_parameters.force_source_part
|
|
1167
|
+
if name:
|
|
1168
|
+
info = self.get_part_info(name)
|
|
1169
|
+
if info is None:
|
|
1170
|
+
raise RuntimeError(f"Unknown force_source_part: {name}")
|
|
1171
|
+
return info
|
|
1172
|
+
piston_info = self._get_piston_info()
|
|
1173
|
+
if piston_info is None:
|
|
1174
|
+
raise RuntimeError(
|
|
1175
|
+
"force_source RIGID_REACTION_FORCE requires a piston part "
|
|
1176
|
+
"or an explicit force_source_part"
|
|
1177
|
+
)
|
|
1178
|
+
return piston_info
|
|
1179
|
+
|
|
1055
1180
|
def _add_to_workflow_von_mises_stress(self, part_infos: list[PartInfo]):
|
|
1056
1181
|
self._add_to_workflow_postprocess(
|
|
1057
1182
|
part_infos, "compress", "von-mises-stress", "cauchy_stress"
|
|
@@ -1065,9 +1190,12 @@ class CompressionSimulation:
|
|
|
1065
1190
|
def _add_to_workflow_force_displacement(self, part_infos: list[PartInfo]):
|
|
1066
1191
|
job_name_base = "force-displacement"
|
|
1067
1192
|
self._add_to_workflow_postprocess(part_infos, "compress", job_name_base)
|
|
1068
|
-
self.
|
|
1069
|
-
|
|
1070
|
-
|
|
1193
|
+
if self.simulation_parameters.force_source == ForceSource.RIGID_REACTION_FORCE:
|
|
1194
|
+
source = self._get_force_source_info()
|
|
1195
|
+
keys = [f"/material{source.material_index}/rigid_reaction_force"]
|
|
1196
|
+
else:
|
|
1197
|
+
keys = ["/boundary_force_zminus"]
|
|
1198
|
+
self.workflow_params[f"{job_name_base}.keys"] = json.dumps(keys)
|
|
1071
1199
|
self.workflow_params[f"{job_name_base}.method"] = "BoundaryForce"
|
|
1072
1200
|
# Find the piston part — there should be exactly one
|
|
1073
1201
|
piston_info = self._get_piston_info()
|
|
@@ -56,7 +56,19 @@ JSON manifest format
|
|
|
56
56
|
|
|
57
57
|
"simulation": {
|
|
58
58
|
"max_time": 0.04,
|
|
59
|
-
"max_resolution": 512
|
|
59
|
+
"max_resolution": 512,
|
|
60
|
+
|
|
61
|
+
# Force source for force-displacement: "boundary_force" (default) or
|
|
62
|
+
# "rigid_reaction_force". When using rigid_reaction_force, optionally
|
|
63
|
+
# name the part it's measured on (defaults to the piston):
|
|
64
|
+
"force_source": "rigid_reaction_force",
|
|
65
|
+
"force_source_part": "puck",
|
|
66
|
+
|
|
67
|
+
# Per-face boundary conditions. Only the faces you list are overridden;
|
|
68
|
+
# the rest keep their defaults (x/y faces "symmetric", z faces
|
|
69
|
+
# "velocity_dirichlet"). Each value is one of: "symmetric",
|
|
70
|
+
# "velocity_dirichlet", "velocity_neumann".
|
|
71
|
+
"boundary_conditions": {"z-": "symmetric", "y+": "velocity_neumann"}
|
|
60
72
|
},
|
|
61
73
|
|
|
62
74
|
"workflow_steps": [
|
|
@@ -100,6 +112,7 @@ from __future__ import annotations
|
|
|
100
112
|
|
|
101
113
|
import json
|
|
102
114
|
import tempfile
|
|
115
|
+
from enum import Enum
|
|
103
116
|
from pathlib import Path
|
|
104
117
|
from typing import Optional, Union
|
|
105
118
|
from zipfile import ZipFile
|
|
@@ -234,6 +247,13 @@ def _build_simulation_parameters(sim_config: dict) -> SimulationParameters:
|
|
|
234
247
|
obj = params
|
|
235
248
|
for part in parts[:-1]:
|
|
236
249
|
obj = getattr(obj, part)
|
|
250
|
+
# Manifest values for enum fields (e.g. force_source) arrive as plain
|
|
251
|
+
# strings; coerce them to the field's enum type. Non-enum fields (e.g.
|
|
252
|
+
# the boundary_conditions dict) pass through and are normalized later
|
|
253
|
+
# in create_sim_config.
|
|
254
|
+
current = getattr(obj, parts[-1], None)
|
|
255
|
+
if isinstance(current, Enum) and isinstance(value, str):
|
|
256
|
+
value = type(current)(value)
|
|
237
257
|
setattr(obj, parts[-1], value)
|
|
238
258
|
return params
|
|
239
259
|
|
|
@@ -2,19 +2,19 @@ metafold/__init__.py,sha256=fDZ2HCBNDOJfs1zAhCse6h2lOiqxl2uUMJT-JuO0XNE,1899
|
|
|
2
2
|
metafold/api.py,sha256=SrlDgvaqy0Vjf9w67OmmOUs3YAQaIEFHb1rpxvpwFWw,1052
|
|
3
3
|
metafold/assets.py,sha256=41OUQ1wKiYitEZMBRPbFfxtM3m4TYb_5S6wXNEbgi40,4569
|
|
4
4
|
metafold/auth.py,sha256=O4B5vGUr2UmHbw_1BfWqZn-GWvmf_eFpZEpKGgBGrQQ,1079
|
|
5
|
-
metafold/client.py,sha256=
|
|
5
|
+
metafold/client.py,sha256=ZFftCr5gbRFkrxgBK7NYspBQEQnNzj2tWCbjPqxg5gw,3714
|
|
6
6
|
metafold/exceptions.py,sha256=dJhBgbryBhJUf20FT3eU1rjQJQ-aDEZ25g-YCn_TwQk,110
|
|
7
7
|
metafold/jobs.py,sha256=DxpQ9yrN7Th67XyW8UqjAIX0wfhwgPPCA9mNSKG_Z_Y,7029
|
|
8
8
|
metafold/materials.py,sha256=heiK_kpU3YhKMPy1P9Zfds8yyUib0yqq3zethufchOc,10814
|
|
9
9
|
metafold/projects.py,sha256=4APFtJQjycFhlIHCWTrOAuU3CLX8drSlgPz8lhVXhzI,6295
|
|
10
10
|
metafold/utils.py,sha256=DnsW1Xr9mLpVHBNdPrdxNLLcjxSNMkWVvr7cH7UTAxY,1174
|
|
11
11
|
metafold/workflows.py,sha256=Mhv3n3ChUHw1Vaj3q8HNDbOgljn03AF6WslZfXzexsI,7802
|
|
12
|
-
metafold/simulation/__init__.py,sha256=
|
|
12
|
+
metafold/simulation/__init__.py,sha256=tUW4PfyWnI6UFvIR__JKUD2FjNTM74fSeKuee7Ewjfo,816
|
|
13
13
|
metafold/simulation/compression_experiment.py,sha256=Dc7EKPAmKEZsNcH5vnKuj3dQcFFeh0hsus9CIA0Ycv4,14221
|
|
14
|
-
metafold/simulation/compression_simulation.py,sha256
|
|
15
|
-
metafold/simulation/run_experiment.py,sha256=
|
|
16
|
-
metafold-0.12.
|
|
17
|
-
metafold-0.12.
|
|
18
|
-
metafold-0.12.
|
|
19
|
-
metafold-0.12.
|
|
20
|
-
metafold-0.12.
|
|
14
|
+
metafold/simulation/compression_simulation.py,sha256=q1LEGR7M3HIPttR1xQwNg5xIUswdHxQjdmczQ95H3nM,102969
|
|
15
|
+
metafold/simulation/run_experiment.py,sha256=jj5tZgOhEdPGZZcJFPL_pELfcFlosTMlWMRxgtur9Uk,14748
|
|
16
|
+
metafold-0.12.dev3.dist-info/licenses/LICENSE,sha256=LejZXzGwe9t0Ezk6g0bRmWUuFSI9vQSk1wJAc1NrrhE,1059
|
|
17
|
+
metafold-0.12.dev3.dist-info/METADATA,sha256=LrUlTJrbM8KcoTWxfbuQpFMLP4HfWYT1WEhnyJynVbM,3495
|
|
18
|
+
metafold-0.12.dev3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
19
|
+
metafold-0.12.dev3.dist-info/top_level.txt,sha256=0dvwa2N6gvl2x4T9c62V4MbYD2soFedI6hm3-lWgyew,9
|
|
20
|
+
metafold-0.12.dev3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|