gemmi-protools 0.1.0__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.

Potentially problematic release.


This version of gemmi-protools might be problematic. Click here for more details.

@@ -0,0 +1,8 @@
1
+ """
2
+ @Author: Luo Jiejian
3
+ """
4
+ from .reader import StructureParser
5
+ from .convert import gemmi2bio, bio2gemmi
6
+ from .align import StructureAligner
7
+ from .ppi import ppi_interface_residues
8
+ from .dockq import dockq_score, dockq_score_interface
@@ -0,0 +1,183 @@
1
+ """
2
+ @Author: Luo Jiejian
3
+ """
4
+ import os
5
+ import pathlib
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ import uuid
11
+ from copy import deepcopy
12
+ from typing import Union, Dict, Any, List, Optional
13
+
14
+ import numpy as np
15
+ from Bio.PDB import Superimposer
16
+ from typeguard import typechecked
17
+
18
+ from .convert import gemmi2bio, bio2gemmi
19
+ from .reader import StructureParser
20
+
21
+
22
+ class StructureAligner(object):
23
+ @typechecked
24
+ def __init__(self, query_path: Union[str, pathlib.Path], ref_path: Union[str, pathlib.Path]):
25
+ self._query_st = StructureParser()
26
+ self._query_st.load_from_file(query_path)
27
+ self._query_st.set_default_model()
28
+
29
+ self._ref_st = StructureParser()
30
+ self._ref_st.load_from_file(ref_path)
31
+ self._ref_st.set_default_model()
32
+
33
+ self.values = dict()
34
+ self.rot_mat = None
35
+ self.is_aligned = False
36
+ self.by_query = None
37
+ self.by_ref = None
38
+ self.query_path = query_path
39
+ self.ref_path = ref_path
40
+
41
+ @property
42
+ def __mmalign_path(self):
43
+ _path = shutil.which("MMAlign") or shutil.which("MMalign")
44
+ if _path is None:
45
+ raise RuntimeError("Executable program MMAlign is not found. "
46
+ "Download from https://zhanggroup.org/MM-align/ ."
47
+ "Build it and add MMAlign to environment PATH")
48
+ else:
49
+ return _path
50
+
51
+ @staticmethod
52
+ @typechecked
53
+ def __parser_rotation_matrix(matrix_file: Union[str, pathlib.Path]):
54
+ rotation_matrix = []
55
+ translation_vector = []
56
+
57
+ with open(matrix_file, 'r') as file:
58
+ lines = file.readlines()
59
+ values = lines[2:5]
60
+ for cur_line in values:
61
+ tmp = re.split(pattern=r"\s+", string=cur_line.strip())
62
+ assert len(tmp) == 5
63
+ rotation_matrix.append(tmp[2:])
64
+ translation_vector.append(tmp[1])
65
+ return dict(R=np.array(rotation_matrix).astype(np.float32),
66
+ T=np.array(translation_vector).astype(np.float32))
67
+
68
+ @staticmethod
69
+ @typechecked
70
+ def __parse_terminal_outputs(output_string: str) -> Dict[str, Any]:
71
+ lines = re.split(pattern=r"\n", string=output_string)
72
+ # chain mapping
73
+ patterns = dict(query_chain_ids=r"Structure_1.+\.pdb:([\w:]+)",
74
+ ref_chain_ids=r"Structure_2.+\.pdb:([\w:]+)",
75
+ query_total_length=r"Length of Structure_1.*?(\d+).*residues",
76
+ ref_total_length=r"Length of Structure_2.*?(\d+).*residues",
77
+ aligned_length=r"Aligned length=.*?(\d+)",
78
+ rmsd=r"RMSD=.*?([\d.]+)",
79
+ tmscore_by_query=r"TM-score=.*?([\d.]+).+Structure_1",
80
+ tmscore_by_ref=r"TM-score=.*?([\d.]+).+Structure_2",
81
+ aligned_seq_start=r"denotes other aligned residues",
82
+ )
83
+
84
+ values = dict()
85
+ for idx, line in enumerate(lines):
86
+ current_keys = list(patterns.keys())
87
+ for key in current_keys:
88
+ tmp = re.search(patterns[key], line)
89
+ if tmp:
90
+ if key in ['query_chain_ids', 'ref_chain_ids']:
91
+ values[key] = re.split(pattern=":", string=tmp.groups()[0])
92
+ del patterns[key]
93
+ elif key in ['query_total_length', 'ref_total_length', 'aligned_length']:
94
+ values[key] = int(tmp.groups()[0])
95
+ del patterns[key]
96
+ elif key in ['rmsd', 'tmscore_by_query', 'tmscore_by_ref']:
97
+ values[key] = float(tmp.groups()[0])
98
+ del patterns[key]
99
+ elif key == "aligned_seq_start":
100
+ # idx + 1 and idx + 3 for aligned sequences 1 and 2
101
+ seq_1 = lines[idx + 1]
102
+ seq_2 = lines[idx + 3]
103
+
104
+ sp1 = re.split(pattern=r"\*", string=seq_1)
105
+ sp2 = re.split(pattern=r"\*", string=seq_2)
106
+ values["query_sequences"] = sp1[:-1] if "*" in seq_1 else sp1
107
+ values["ref_sequences"] = sp2[:-1] if "*" in seq_2 else sp2
108
+ del patterns[key]
109
+ return values
110
+
111
+ @typechecked
112
+ def make_alignment(self, query_chains: Optional[List[str]] = None,
113
+ ref_chains: Optional[List[str]] = None, timeout=300.0):
114
+ """
115
+
116
+ :param query_chains: list, None for all chains
117
+ :param ref_chains: list, None for all chains
118
+ :param timeout: default 300
119
+ :return:
120
+ """
121
+
122
+ program_path = self.__mmalign_path
123
+
124
+ # clone
125
+ q_st = deepcopy(self._query_st)
126
+ r_st = deepcopy(self._ref_st)
127
+
128
+ tmp_dir = os.path.join(tempfile.gettempdir(), str(uuid.uuid4()))
129
+ os.makedirs(tmp_dir)
130
+
131
+ if isinstance(query_chains, list):
132
+ q_st.pick_chains(query_chains)
133
+
134
+ if isinstance(ref_chains, list):
135
+ r_st.pick_chains(ref_chains)
136
+
137
+ _tmp_a = os.path.join(tmp_dir, "a.pdb")
138
+ q_st.to_pdb(_tmp_a)
139
+
140
+ _tmp_b = os.path.join(tmp_dir, "b.pdb")
141
+ r_st.to_pdb(_tmp_b)
142
+
143
+ matrix_file = os.path.join(tmp_dir, "m.txt")
144
+ _command = "%s %s %s -m %s" % (program_path, _tmp_a, _tmp_b, matrix_file)
145
+
146
+ try:
147
+ result = subprocess.run(_command, shell=True, check=True,
148
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
149
+ timeout=timeout)
150
+ except Exception as e:
151
+ print("%s: between files %s and %s; between chains: %s and %s" % (
152
+ str(e), self.query_path, self.ref_path,
153
+ str(q_st.chain_ids), str(r_st.chain_ids))
154
+ )
155
+ else:
156
+ self.values = self.__parse_terminal_outputs(result.stdout.decode())
157
+ self.rot_mat = self.__parser_rotation_matrix(matrix_file)
158
+ self.is_aligned = True
159
+ self.by_query = q_st.chain_ids if query_chains is None else query_chains
160
+ self.by_ref = r_st.chain_ids if ref_chains is None else ref_chains
161
+ finally:
162
+ if os.path.isdir(tmp_dir):
163
+ shutil.rmtree(tmp_dir)
164
+
165
+ @typechecked
166
+ def save_aligned_query(self, out_file: str):
167
+ """
168
+
169
+ :param out_file: .cif file
170
+ :return:
171
+ """
172
+ if not self.is_aligned:
173
+ raise RuntimeError("structure not aligned, run make_alignment first")
174
+
175
+ super_imposer = Superimposer()
176
+ super_imposer.rotran = (self.rot_mat["R"].T, self.rot_mat["T"])
177
+
178
+ bio_s = gemmi2bio(self._query_st.STRUCT)
179
+ super_imposer.apply(bio_s)
180
+ query_st_aligned = bio2gemmi(bio_s)
181
+
182
+ block = query_st_aligned.make_mmcif_block()
183
+ block.write_file(out_file)
@@ -0,0 +1,167 @@
1
+ """
2
+ @Author: Luo Jiejian
3
+ """
4
+
5
+ import pathlib
6
+ from typing import Union, Dict, Any
7
+
8
+ import gemmi
9
+ import pandas as pd
10
+ from typeguard import typechecked
11
+
12
+ from .struct_info import Entity
13
+
14
+
15
+ @typechecked
16
+ def _is_cif(path: Union[str, pathlib.Path]) -> bool:
17
+ if isinstance(path, str):
18
+ path = pathlib.Path(path)
19
+ if path.suffixes:
20
+ if path.suffixes[-1] == ".cif":
21
+ return True
22
+ elif "".join(path.suffixes[-2:]) == ".cif.gz":
23
+ return True
24
+ else:
25
+ return False
26
+ else:
27
+ return False
28
+
29
+
30
+ @typechecked
31
+ def _value_mapper_from_block(block: gemmi.cif.Block, category: str, column1: str, column2: str,
32
+ expand_column1: bool = False) -> Dict[str, Any]:
33
+ """
34
+ mapper from column1 to column2
35
+ :param block:
36
+ :param category:
37
+ :param column1:
38
+ :param column2:
39
+ :param expand_column1: bool, if True, values joint by comma in column1 with be split
40
+ :return:
41
+ Only return a mapper when both column1 and column2 in category
42
+ """
43
+ loop = block.find_mmcif_category(category)
44
+ tags = list(loop.tags)
45
+
46
+ results = dict()
47
+ if column1 in tags:
48
+ values1 = loop.column(tags.index(column1))
49
+ v1 = [values1.str(i) for i in range(len(values1))]
50
+
51
+ if column2 in tags:
52
+ values2 = loop.column(tags.index(column2))
53
+ v2 = [values2.str(i) for i in range(len(values2))]
54
+ else:
55
+ v2 = ["?"] * len(v1)
56
+
57
+ outputs = dict(zip(v1, v2))
58
+
59
+ if expand_column1:
60
+ outputs_ex = dict()
61
+ for key, val in outputs.items():
62
+ tmp = key.split(",")
63
+ for sk in tmp:
64
+ nk = sk.strip()
65
+ if nk:
66
+ outputs_ex[nk] = val
67
+ results = outputs_ex
68
+ else:
69
+ results = outputs
70
+ return results
71
+
72
+
73
+ @typechecked
74
+ def _get_cif_resolution(block: gemmi.cif.Block) -> float:
75
+ resolution = 0.0
76
+ for key in ["_reflns.d_resolution_high",
77
+ "_refine.ls_d_res_high",
78
+ "_refine_hist.d_res_high",
79
+ "_em_3d_reconstruction.resolution",
80
+ ]:
81
+ v = block.find_value(key)
82
+ try:
83
+ vf = float(v)
84
+ except (TypeError, ValueError):
85
+ continue
86
+ else:
87
+ resolution = vf
88
+ break
89
+ return resolution
90
+
91
+
92
+ @typechecked
93
+ def _cif_entity_info(block: gemmi.cif.Block) -> Entity:
94
+ entity2description = _value_mapper_from_block(block, category="_entity.",
95
+ column1="_entity.id",
96
+ column2="_entity.pdbx_description")
97
+
98
+ polymer2entity = _value_mapper_from_block(block, category="_entity_poly.",
99
+ column1="_entity_poly.pdbx_strand_id",
100
+ column2="_entity_poly.entity_id",
101
+ expand_column1=True)
102
+ entity2species = _value_mapper_from_block(block, category="_entity_src_gen.",
103
+ column1="_entity_src_gen.entity_id",
104
+ column2="_entity_src_gen.pdbx_gene_src_scientific_name")
105
+
106
+ entity2species.update(_value_mapper_from_block(block, category="_pdbx_entity_src_syn.",
107
+ column1="_pdbx_entity_src_syn.entity_id",
108
+ column2="_pdbx_entity_src_syn.organism_scientific")
109
+ )
110
+ entity2species.update(_value_mapper_from_block(block, category="_entity_src_nat.",
111
+ column1="_entity_src_nat.entity_id",
112
+ column2="_entity_src_nat.pdbx_organism_scientific")
113
+ )
114
+ entity2taxid = _value_mapper_from_block(block, category="_entity_src_gen.",
115
+ column1="_entity_src_gen.entity_id",
116
+ column2="_entity_src_gen.pdbx_gene_src_ncbi_taxonomy_id")
117
+ entity2taxid.update(_value_mapper_from_block(block, category="_pdbx_entity_src_syn.",
118
+ column1="_pdbx_entity_src_syn.entity_id",
119
+ column2="_pdbx_entity_src_syn.ncbi_taxonomy_id")
120
+ )
121
+ entity2taxid.update(_value_mapper_from_block(block, category="_entity_src_nat.",
122
+ column1="_entity_src_nat.entity_id",
123
+ column2="_entity_src_nat.pdbx_ncbi_taxonomy_id")
124
+ )
125
+
126
+ vals = dict(eid2desc=entity2description,
127
+ eid2specie=entity2species,
128
+ eid2taxid=entity2taxid,
129
+ polymer2eid=polymer2entity
130
+ )
131
+ return Entity(**vals)
132
+
133
+
134
+ @typechecked
135
+ def _cif_block_for_output(structure: gemmi.Structure, entity: Entity) -> gemmi.cif.Block:
136
+ block = structure.make_mmcif_block()
137
+
138
+ ta = block.find_mmcif_category(category="_entity.")
139
+ da = pd.DataFrame(list(ta), columns=list(ta.tags))
140
+ if "_entity.id" in da.columns:
141
+ da["_entity.pdbx_description"] = da["_entity.id"].apply(
142
+ lambda i: entity["eid2desc"].get(i, "?").strip() or "?")
143
+
144
+ rows = []
145
+ for ar in da.to_numpy().tolist():
146
+ rows.append([gemmi.cif.quote(i) for i in ar])
147
+
148
+ if "_entity.pdbx_description" not in list(ta.tags):
149
+ ta.loop.add_columns(["_entity.pdbx_description"], "?")
150
+
151
+ ta = block.find_mmcif_category(category="_entity.")
152
+ for _ in range(len(ta)):
153
+ ta.remove_row(0)
154
+ for row in rows:
155
+ ta.append_row(row)
156
+
157
+ loop = block.init_loop("_entity_src_gen.", ["entity_id",
158
+ "pdbx_gene_src_scientific_name",
159
+ "pdbx_gene_src_ncbi_taxonomy_id"])
160
+
161
+ for k in entity["eid2specie"].keys():
162
+ loop.add_row([gemmi.cif.quote(k),
163
+ gemmi.cif.quote(entity["eid2specie"].get(k, "?")),
164
+ gemmi.cif.quote(entity["eid2taxid"].get(k, "?"))]
165
+ )
166
+ block.move_item(-1, 16)
167
+ return block
@@ -0,0 +1,96 @@
1
+ """
2
+ @Author: Luo Jiejian
3
+ """
4
+
5
+ import gemmi
6
+ import numpy as np
7
+ from Bio.PDB.Structure import Structure as BioStructure
8
+ from Bio.PDB.StructureBuilder import StructureBuilder
9
+ from typeguard import typechecked
10
+
11
+
12
+ @typechecked
13
+ def gemmi2bio(gemmi_structure: gemmi.Structure) -> BioStructure:
14
+ """
15
+ Convert gemmi structure to biopython structure
16
+ :param gemmi_structure:
17
+ :return:
18
+ return biopython structure
19
+ """
20
+ structure_builder = StructureBuilder()
21
+ structure_builder.init_structure(structure_id=gemmi_structure.name)
22
+
23
+ for model_idx, gemmi_model in enumerate(gemmi_structure):
24
+ structure_builder.init_model(model_idx)
25
+
26
+ for gemmi_chain in gemmi_model:
27
+ structure_builder.init_chain(gemmi_chain.name)
28
+
29
+ for gemmi_residue in gemmi_chain:
30
+ if gemmi_residue.het_flag == "H":
31
+ if gemmi_residue.name in ["HOH", "WAT"]:
32
+ het_flag = "W"
33
+ else:
34
+ het_flag = "H"
35
+ else:
36
+ het_flag = " "
37
+
38
+ structure_builder.init_residue(resname=gemmi_residue.name, field=het_flag,
39
+ resseq=gemmi_residue.seqid.num, icode=gemmi_residue.seqid.icode)
40
+ for gemmi_atom in gemmi_residue:
41
+ coord = np.array([gemmi_atom.pos.x, gemmi_atom.pos.y, gemmi_atom.pos.z])
42
+ structure_builder.init_atom(name=gemmi_atom.name,
43
+ coord=coord,
44
+ b_factor=gemmi_atom.b_iso,
45
+ occupancy=gemmi_atom.occ,
46
+ altloc=gemmi_atom.altloc if gemmi_atom.has_altloc() else ' ',
47
+ fullname=gemmi_atom.name.center(4),
48
+ serial_number=gemmi_atom.serial,
49
+ element=gemmi_atom.element.name.upper())
50
+
51
+ bio_structure = structure_builder.get_structure()
52
+ return bio_structure
53
+
54
+
55
+ @typechecked
56
+ def bio2gemmi(bio_structure: BioStructure) -> gemmi.Structure:
57
+ """
58
+ Convert biopython structure to gemmi structure
59
+ :param bio_structure:
60
+ :return:
61
+ return gemmi structure
62
+ """
63
+
64
+ g_structure = gemmi.Structure()
65
+ g_structure.name = bio_structure.id
66
+
67
+ for bio_model in bio_structure:
68
+ # bio model start from 0, gemmi model start from 1
69
+ g_model = gemmi.Model(bio_model.id + 1)
70
+ for bio_chain in bio_model:
71
+ g_chain = gemmi.Chain(bio_chain.id)
72
+ for bio_residue in bio_chain:
73
+ g_residue = gemmi.Residue()
74
+ g_residue.name = bio_residue.resname
75
+ het_flag, r_num, i_code = bio_residue.id
76
+ g_residue.seqid.num = r_num
77
+ g_residue.seqid.icode = i_code
78
+ g_residue.het_flag = "A" if het_flag == " " else "H"
79
+
80
+ for bio_atom in bio_residue:
81
+ g_atom = gemmi.Atom()
82
+ g_atom.name = bio_atom.name
83
+ g_atom.b_iso = bio_atom.bfactor
84
+ g_atom.occ = bio_atom.occupancy
85
+ g_atom.altloc = "\x00" if bio_atom.altloc == " " else bio_atom.altloc
86
+ g_atom.element = gemmi.Element(bio_atom.element)
87
+ g_atom.serial = bio_atom.serial_number
88
+ px, py, pz = bio_atom.coord
89
+ g_atom.pos = gemmi.Position(px, py, pz)
90
+ g_residue.add_atom(g_atom)
91
+ g_chain.add_residue(g_residue)
92
+ g_model.add_chain(g_chain)
93
+ g_structure.add_model(g_model)
94
+ g_structure.setup_entities()
95
+ g_structure.assign_het_flags()
96
+ return g_structure
@@ -0,0 +1,139 @@
1
+ """
2
+ @Author: Luo Jiejian
3
+ """
4
+ import json
5
+ import os
6
+ import pathlib
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ import uuid
11
+ from typing import Optional, Union
12
+
13
+ import pandas as pd
14
+ from typeguard import typechecked
15
+
16
+ from .reader import StructureParser
17
+
18
+
19
+ @typechecked
20
+ def _read_model(model_file: Union[str, pathlib.Path]):
21
+ st = StructureParser()
22
+ st.load_from_file(model_file)
23
+ st.set_default_model()
24
+ return st
25
+
26
+
27
+ @typechecked
28
+ def dockq_score(query_model: Union[str, pathlib.Path],
29
+ native_model: Union[str, pathlib.Path],
30
+ mapping: Optional[str] = None):
31
+ dockq_program = shutil.which("DockQ")
32
+ if dockq_program is None:
33
+ raise RuntimeError("DockQ is need")
34
+
35
+ q_st = _read_model(query_model)
36
+ n_st = _read_model(native_model)
37
+
38
+ tmp_dir = os.path.join(tempfile.gettempdir(), str(uuid.uuid4()))
39
+ os.makedirs(tmp_dir)
40
+
41
+ result_file = os.path.join(tmp_dir, "result.json")
42
+ q_file = os.path.join(tmp_dir, "q.pdb")
43
+ n_file = os.path.join(tmp_dir, "n.pdb")
44
+ q_st.to_pdb(q_file, write_minimal_pdb=True)
45
+ n_st.to_pdb(n_file, write_minimal_pdb=True)
46
+ if mapping is None:
47
+ cid = "".join(n_st.chain_ids)
48
+ mapping = cid + ":" + cid
49
+
50
+ _command = "%s --mapping %s --json %s %s %s" % (dockq_program, mapping, result_file, q_file, n_file)
51
+ metrics = ['DockQ', 'F1', 'chain1', 'chain2']
52
+
53
+ try:
54
+ _ = subprocess.run(_command, shell=True, check=True,
55
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
56
+ timeout=300.0)
57
+ except subprocess.CalledProcessError as e:
58
+ # Handle errors in the called executable
59
+ msg = e.stderr.decode()
60
+ outputs = pd.DataFrame(columns=metrics)
61
+ except Exception as e:
62
+ # Handle other exceptions such as file not found or permissions issues
63
+ msg = str(e)
64
+ outputs = pd.DataFrame(columns=metrics)
65
+ else:
66
+ with open(result_file, "r") as fin:
67
+ vals = json.load(fin)
68
+ msg = "Finished"
69
+ result = []
70
+ for v in vals["best_result"].values():
71
+ result.append(v)
72
+ outputs = pd.DataFrame(result)[metrics]
73
+ finally:
74
+ if os.path.isdir(tmp_dir):
75
+ shutil.rmtree(tmp_dir)
76
+
77
+ return dict(value=outputs,
78
+ msg=msg,
79
+ mapping=mapping,
80
+ model=query_model,
81
+ native=native_model
82
+ )
83
+
84
+
85
+ def dockq_score_interface(query_model: Union[str, pathlib.Path],
86
+ native_model: Union[str, pathlib.Path],
87
+ chains_a: str,
88
+ chains_b: str):
89
+ ppi_if = chains_a + "@" + chains_b
90
+ chs_a = list(chains_a)
91
+ chs_b = list(chains_b)
92
+
93
+ # if multiple chains, merge to one
94
+ q_st = _read_model(query_model)
95
+ n_st = _read_model(native_model)
96
+
97
+ for c in chs_a + chs_b:
98
+ if c not in q_st.chain_ids:
99
+ raise RuntimeError("Chain %s is not in the query model: %s" % (c, query_model))
100
+
101
+ for c in chs_a + chs_b:
102
+ if c not in n_st.chain_ids:
103
+ raise RuntimeError("Chain %s is not in the native model: %s" % (c, native_model))
104
+
105
+ if len(chs_a) > 1:
106
+ q_st.merge_chains(chs_a)
107
+ n_st.merge_chains(chs_a)
108
+
109
+ if len(chs_b) > 1:
110
+ q_st.merge_chains(chs_b)
111
+ n_st.merge_chains(chs_b)
112
+
113
+ tmp_dir = os.path.join(tempfile.gettempdir(), str(uuid.uuid4()))
114
+ os.makedirs(tmp_dir)
115
+
116
+ q_file = os.path.join(tmp_dir, "qm.pdb")
117
+ n_file = os.path.join(tmp_dir, "nm.pdb")
118
+ q_st.to_pdb(q_file, write_minimal_pdb=True)
119
+ n_st.to_pdb(n_file, write_minimal_pdb=True)
120
+
121
+ chs = chs_a[0] + chs_b[0]
122
+ result = dockq_score(q_file, n_file, mapping="%s:%s" % (chs, chs))
123
+
124
+ if len(result["value"]) > 0:
125
+ q_score = round(result["value"].iloc[0]["DockQ"], 4)
126
+ f1 = round(result["value"].iloc[0]["F1"], 4)
127
+ else:
128
+ q_score = ""
129
+ f1 = ""
130
+
131
+ if os.path.isdir(tmp_dir):
132
+ shutil.rmtree(tmp_dir)
133
+
134
+ return dict(DockQ=q_score,
135
+ F1=f1,
136
+ interface=ppi_if,
137
+ model=query_model,
138
+ native=native_model
139
+ )